Compare commits

..
Author SHA1 Message Date
Nicolò Boschi 3d66eb02a8 feat(cli): accept more file types on retain-files 2026-01-15 16:21:00 +01:00
Nicolò Boschi b638d80c89 feat(cli): accept more file types on retain-files 2026-01-15 12:38:43 +01:00
Chris Bartholomew 55c216e069 Fix skill installer test examples to use meaningful content (#160)
The "Test memory" example is too short for the LLM to extract
meaningful facts from, causing the test to silently fail (0 memories
created). Replace with "Alice works at Google as a software engineer"
which has enough context for fact extraction.

Fixes test examples in:
- get-skill installer (local and cloud modes)
- hindsight-embed configure output
- skills.md documentation
2026-01-14 18:41:04 +01:00
Chris Bartholomew e64d3634a9 feat: add cloud mode to skill installer for team memory sharing (#158)
* doc: update expired Slack invite link

* feat: add cloud mode to skill installer for team memory sharing

Adds support for Hindsight Cloud in the skill installer, enabling teams
to share memories about a codebase. Changes include:

- Add `--mode cloud` option to get-skill installer
- Install hindsight CLI binary for cloud mode (via get-cli)
- Configure ~/.hindsight/config with API URL and key
- Generate cloud-specific SKILL.md with team-aware guidance
- Distinguish between project conventions and individual preferences
- Update skills.md documentation with cloud setup instructions

Cloud mode workflow:
1. Team admin creates a bank in Hindsight Cloud
2. Each developer runs: curl ... | bash -s -- --mode cloud
3. All team members share the same memory bank
4. Knowledge retained by one member benefits everyone
2026-01-14 09:05:40 +01:00
20 changed files with 1122 additions and 464 deletions
+14 -26
View File
@@ -153,8 +153,15 @@ jobs:
- name: Build docs
run: npm run build --workspace=hindsight-docs
build-rust-cli:
test-rust-cli:
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
HINDSIGHT_API_URL: http://localhost:8888
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
steps:
- uses: actions/checkout@v4
@@ -171,6 +178,10 @@ jobs:
hindsight-cli/target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Run unit tests
working-directory: hindsight-cli
run: cargo test
- name: Build CLI
working-directory: hindsight-cli
run: cargo build --release
@@ -182,29 +193,6 @@ jobs:
path: hindsight-cli/target/release/hindsight
retention-days: 1
test-rust-cli:
runs-on: ubuntu-latest
needs: build-rust-cli
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
HINDSIGHT_API_URL: http://localhost:8888
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
steps:
- uses: actions/checkout@v4
- name: Download CLI artifact
uses: actions/download-artifact@v4
with:
name: hindsight-cli
path: /tmp/cli
- name: Make CLI executable
run: chmod +x /tmp/cli/hindsight
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
@@ -251,7 +239,7 @@ jobs:
- name: Run CLI smoke test
run: |
HINDSIGHT_CLI=/tmp/cli/hindsight ./hindsight-cli/smoke-test.sh
HINDSIGHT_CLI=hindsight-cli/target/release/hindsight ./hindsight-cli/smoke-test.sh
- name: Show API server logs
if: always()
@@ -777,7 +765,7 @@ jobs:
test-doc-examples:
runs-on: ubuntu-latest
needs: build-rust-cli
needs: test-rust-cli
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
+147
View File
@@ -55,6 +55,7 @@ pub struct MemoryPutResult {
pub items_count: i64,
pub message: String,
pub is_async: bool,
pub operation_id: Option<String>,
}
#[derive(Clone)]
@@ -161,10 +162,54 @@ impl ApiClient {
items_count: result.items_count,
message: format!("Stored {} memory units", result.items_count),
is_async: result.async_,
operation_id: result.operation_id,
})
})
}
/// Poll an operation until it completes or fails.
/// Returns Ok(true) if completed successfully, Ok(false) if failed, Err if polling error.
pub fn poll_operation(&self, agent_id: &str, operation_id: &str, verbose: bool) -> Result<(bool, Option<String>)> {
self.runtime.block_on(async {
loop {
let response = self.client.list_operations(agent_id, None).await?;
let ops = response.into_inner();
// Find our operation
let op = ops.operations.iter().find(|o| o.id == operation_id);
match op {
Some(operation) => {
if verbose {
eprintln!("Operation {} status: {}", operation_id, operation.status);
}
match operation.status.as_str() {
"pending" => {
// Still running, wait and poll again
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
"completed" => {
// Operation completed successfully
return Ok((true, None));
}
"failed" => {
return Ok((false, operation.error_message.clone()));
}
_ => {
// Unknown status, treat as failed
return Ok((false, Some(format!("Unknown status: {}", operation.status))));
}
}
}
None => {
// Operation not in list means it completed successfully (removed from pending/failed)
return Ok((true, None));
}
}
}
})
}
pub fn delete_memory(&self, _agent_id: &str, _unit_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
// Note: Individual memory deletion is no longer supported in the API
anyhow::bail!("Individual memory deletion is no longer supported. Use 'memory clear' to clear all memories.")
@@ -281,3 +326,105 @@ pub use types::{
ReflectResponse,
RetainRequest,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_deserialize() {
let json = r#"{
"id": "test-op-123",
"task_type": "retain",
"items_count": 5,
"document_id": "doc-456",
"created_at": "2024-01-15T10:00:00Z",
"status": "pending",
"error_message": null
}"#;
let op: Operation = serde_json::from_str(json).unwrap();
assert_eq!(op.id, "test-op-123");
assert_eq!(op.task_type, "retain");
assert_eq!(op.items_count, 5);
assert_eq!(op.document_id, Some("doc-456".to_string()));
assert_eq!(op.status, "pending");
assert!(op.error_message.is_none());
}
#[test]
fn test_operation_deserialize_with_error() {
let json = r#"{
"id": "test-op-456",
"task_type": "retain",
"items_count": 3,
"document_id": null,
"created_at": "2024-01-15T10:00:00Z",
"status": "failed",
"error_message": "Something went wrong"
}"#;
let op: Operation = serde_json::from_str(json).unwrap();
assert_eq!(op.status, "failed");
assert_eq!(op.error_message, Some("Something went wrong".to_string()));
}
#[test]
fn test_memory_put_result_serialize() {
let result = MemoryPutResult {
success: true,
items_count: 10,
message: "Stored 10 memory units".to_string(),
is_async: true,
operation_id: Some("op-789".to_string()),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"items_count\":10"));
assert!(json.contains("\"is_async\":true"));
assert!(json.contains("\"operation_id\":\"op-789\""));
}
#[test]
fn test_memory_put_result_without_operation_id() {
let result = MemoryPutResult {
success: true,
items_count: 5,
message: "Stored 5 memory units".to_string(),
is_async: false,
operation_id: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"operation_id\":null"));
}
#[test]
fn test_operations_response_deserialize() {
let json = r#"{
"bank_id": "test-bank",
"operations": [
{
"id": "op-1",
"task_type": "retain",
"items_count": 2,
"document_id": null,
"created_at": "2024-01-15T10:00:00Z",
"status": "pending",
"error_message": null
},
{
"id": "op-2",
"task_type": "retain",
"items_count": 3,
"document_id": "doc-123",
"created_at": "2024-01-15T11:00:00Z",
"status": "completed",
"error_message": null
}
]
}"#;
let ops: OperationsResponse = serde_json::from_str(json).unwrap();
assert_eq!(ops.bank_id, "test-bank");
assert_eq!(ops.operations.len(), 2);
assert_eq!(ops.operations[0].status, "pending");
assert_eq!(ops.operations[1].status, "completed");
}
}
+151 -25
View File
@@ -21,6 +21,17 @@ fn parse_budget(budget: &str) -> Budget {
}
}
// Helper function to check if a file has a text-based extension
fn is_text_file(path: &std::path::Path) -> bool {
const TEXT_EXTENSIONS: &[&str] = &[
"txt", "md", "json", "yaml", "yml", "toml", "xml", "csv", "log", "rst", "adoc",
];
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| TEXT_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
.unwrap_or(false)
}
pub fn recall(
client: &ApiClient,
agent_id: &str,
@@ -229,29 +240,23 @@ pub fn retain_files(
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "txt" || ext == "md" {
files.push(path.to_path_buf());
}
if is_text_file(&path) {
files.push(path.to_path_buf());
}
}
} else {
for entry in fs::read_dir(&path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "txt" || ext == "md" {
files.push(path);
}
}
if path.is_file() && is_text_file(&path) {
files.push(path);
}
}
}
}
if files.is_empty() {
ui::print_warning("No .txt or .md files found");
ui::print_warning("No text files found (supported: txt, md, json, yaml, yml, toml, xml, csv, log, rst, adoc)");
return Ok(());
}
@@ -286,19 +291,20 @@ pub fn retain_files(
pb.finish_with_message("Files processed");
// Always use async mode for the API call
let request = RetainRequest {
items,
async_: true,
document_tags: None,
};
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Retaining memories..."))
Some(ui::create_spinner("Submitting retain request..."))
} else {
None
};
let request = RetainRequest {
items,
async_: r#async,
document_tags: None,
};
let response = client.retain(agent_id, &request, r#async, verbose);
let response = client.retain(agent_id, &request, true, verbose);
if let Some(mut sp) = spinner {
sp.finish();
@@ -306,16 +312,55 @@ pub fn retain_files(
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_success("Files retained successfully");
if result.is_async {
println!(" Status: queued for background processing");
if r#async {
// User requested async mode - return immediately
if output_format == OutputFormat::Pretty {
ui::print_success("Files queued for processing");
println!(" Items: {}", result.items_count);
if let Some(op_id) = &result.operation_id {
println!(" Operation ID: {}", op_id);
}
} else {
println!(" Total units created: {}", result.items_count);
output::print_output(&result, output_format)?;
}
} else {
output::print_output(&result, output_format)?;
// Poll until completion
if let Some(operation_id) = &result.operation_id {
let poll_spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Processing memories..."))
} else {
None
};
let (success, error_msg) = client.poll_operation(agent_id, operation_id, verbose)?;
if let Some(mut sp) = poll_spinner {
sp.finish();
}
if success {
if output_format == OutputFormat::Pretty {
ui::print_success("Files retained successfully");
println!(" Items processed: {}", result.items_count);
} else {
output::print_output(&result, output_format)?;
}
} else {
let msg = error_msg.unwrap_or_else(|| "Unknown error".to_string());
if output_format == OutputFormat::Pretty {
ui::print_error(&format!("Retain operation failed: {}", msg));
}
anyhow::bail!("Retain operation failed: {}", msg);
}
} else {
// No operation ID returned, shouldn't happen with async=true
if output_format == OutputFormat::Pretty {
ui::print_success("Files retained successfully");
println!(" Items processed: {}", result.items_count);
} else {
output::print_output(&result, output_format)?;
}
}
}
Ok(())
}
@@ -428,3 +473,84 @@ pub fn clear(
Err(e) => Err(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_is_text_file_supported_extensions() {
let supported = [
"file.txt", "file.md", "file.json", "file.yaml", "file.yml",
"file.toml", "file.xml", "file.csv", "file.log", "file.rst", "file.adoc",
];
for filename in supported {
assert!(
is_text_file(Path::new(filename)),
"{} should be recognized as a text file",
filename
);
}
}
#[test]
fn test_is_text_file_case_insensitive() {
assert!(is_text_file(Path::new("file.JSON")));
assert!(is_text_file(Path::new("file.TXT")));
assert!(is_text_file(Path::new("file.Md")));
assert!(is_text_file(Path::new("file.YAML")));
}
#[test]
fn test_is_text_file_unsupported_extensions() {
let unsupported = [
"file.pdf", "file.doc", "file.docx", "file.png", "file.jpg",
"file.exe", "file.bin", "file.zip", "file.tar", "file.gz",
];
for filename in unsupported {
assert!(
!is_text_file(Path::new(filename)),
"{} should NOT be recognized as a text file",
filename
);
}
}
#[test]
fn test_is_text_file_no_extension() {
assert!(!is_text_file(Path::new("README")));
assert!(!is_text_file(Path::new("Makefile")));
assert!(!is_text_file(Path::new(".gitignore")));
}
#[test]
fn test_is_text_file_with_path() {
assert!(is_text_file(Path::new("/some/path/to/file.json")));
assert!(is_text_file(Path::new("../relative/path/file.md")));
assert!(!is_text_file(Path::new("/path/to/image.png")));
}
#[test]
fn test_parse_budget_valid_values() {
assert!(matches!(parse_budget("low"), Budget::Low));
assert!(matches!(parse_budget("mid"), Budget::Mid));
assert!(matches!(parse_budget("high"), Budget::High));
}
#[test]
fn test_parse_budget_case_insensitive() {
assert!(matches!(parse_budget("LOW"), Budget::Low));
assert!(matches!(parse_budget("MID"), Budget::Mid));
assert!(matches!(parse_budget("HIGH"), Budget::High));
assert!(matches!(parse_budget("Low"), Budget::Low));
assert!(matches!(parse_budget("High"), Budget::High));
}
#[test]
fn test_parse_budget_defaults_to_mid() {
assert!(matches!(parse_budget("invalid"), Budget::Mid));
assert!(matches!(parse_budget(""), Budget::Mid));
assert!(matches!(parse_budget("unknown"), Budget::Mid));
}
}
+154
View File
@@ -8,6 +8,7 @@ const DEFAULT_API_URL: &str = "http://localhost:8888";
const CONFIG_FILE_NAME: &str = "config";
const CONFIG_DIR_NAME: &str = ".hindsight";
#[derive(Debug)]
pub struct Config {
pub api_url: String,
pub api_key: Option<String>,
@@ -174,3 +175,156 @@ pub fn generate_doc_id() -> String {
let now = chrono::Local::now();
format!("cli_put_{}", now.format("%Y%m%d_%H%M%S"))
}
/// Parse a simple TOML-like config line and extract value.
/// Handles both quoted and unquoted values.
pub fn parse_config_value(line: &str, key: &str) -> Option<String> {
let line = line.trim();
if !line.starts_with(key) {
return None;
}
line.split('=').nth(1).map(|value| {
value.trim().trim_matches('"').trim_matches('\'').to_string()
}).filter(|v| !v.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_source_display() {
assert_eq!(format!("{}", ConfigSource::LocalFile), "config file");
assert_eq!(format!("{}", ConfigSource::Environment), "environment variable");
assert_eq!(format!("{}", ConfigSource::Default), "default");
}
#[test]
fn test_validate_and_create_valid_http() {
let config = Config::validate_and_create(
"http://localhost:8888".to_string(),
None,
ConfigSource::Default,
);
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.api_url, "http://localhost:8888");
assert_eq!(config.source, ConfigSource::Default);
}
#[test]
fn test_validate_and_create_valid_https() {
let config = Config::validate_and_create(
"https://api.example.com".to_string(),
Some("secret-key".to_string()),
ConfigSource::Environment,
);
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.api_url, "https://api.example.com");
assert_eq!(config.api_key, Some("secret-key".to_string()));
assert_eq!(config.source, ConfigSource::Environment);
}
#[test]
fn test_validate_and_create_invalid_url() {
let config = Config::validate_and_create(
"localhost:8888".to_string(),
None,
ConfigSource::Default,
);
assert!(config.is_err());
let err = config.unwrap_err().to_string();
assert!(err.contains("Invalid API URL"));
assert!(err.contains("Must start with http:// or https://"));
}
#[test]
fn test_validate_and_create_ftp_url() {
let config = Config::validate_and_create(
"ftp://example.com".to_string(),
None,
ConfigSource::Default,
);
assert!(config.is_err());
}
#[test]
fn test_generate_doc_id_format() {
let doc_id = generate_doc_id();
assert!(doc_id.starts_with("cli_put_"));
// Should be cli_put_YYYYMMDD_HHMMSS format
assert!(doc_id.len() > 20); // cli_put_ (8) + date (8) + _ (1) + time (6) = 23
}
#[test]
fn test_generate_doc_id_uniqueness() {
let id1 = generate_doc_id();
std::thread::sleep(std::time::Duration::from_secs(1));
let id2 = generate_doc_id();
// IDs generated at different times should be different
assert_ne!(id1, id2);
}
#[test]
fn test_parse_config_value_quoted() {
assert_eq!(
parse_config_value(r#"api_url = "http://localhost:8888""#, "api_url"),
Some("http://localhost:8888".to_string())
);
}
#[test]
fn test_parse_config_value_single_quoted() {
assert_eq!(
parse_config_value("api_url = 'http://localhost:8888'", "api_url"),
Some("http://localhost:8888".to_string())
);
}
#[test]
fn test_parse_config_value_unquoted() {
assert_eq!(
parse_config_value("api_url = http://localhost:8888", "api_url"),
Some("http://localhost:8888".to_string())
);
}
#[test]
fn test_parse_config_value_with_spaces() {
assert_eq!(
parse_config_value(" api_url = \"http://localhost:8888\" ", "api_url"),
Some("http://localhost:8888".to_string())
);
}
#[test]
fn test_parse_config_value_wrong_key() {
assert_eq!(
parse_config_value("api_key = secret", "api_url"),
None
);
}
#[test]
fn test_parse_config_value_empty() {
assert_eq!(
parse_config_value("api_url = ", "api_url"),
None
);
assert_eq!(
parse_config_value("api_url = \"\"", "api_url"),
None
);
}
#[test]
fn test_config_api_url_accessor() {
let config = Config {
api_url: "http://test:8080".to_string(),
api_key: None,
source: ConfigSource::Default,
};
assert_eq!(config.api_url(), "http://test:8080");
}
}
+142 -2
View File
@@ -8,13 +8,35 @@ pub enum OutputFormat {
Yaml,
}
impl OutputFormat {
/// Parse output format from string
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"json" => Some(OutputFormat::Json),
"yaml" | "yml" => Some(OutputFormat::Yaml),
"pretty" | "text" => Some(OutputFormat::Pretty),
_ => None,
}
}
}
/// Format data as JSON string
pub fn to_json<T: Serialize>(data: &T) -> Result<String> {
Ok(serde_json::to_string_pretty(data)?)
}
/// Format data as YAML string
pub fn to_yaml<T: Serialize>(data: &T) -> Result<String> {
Ok(serde_yaml::to_string(data)?)
}
pub fn print_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(data)?);
println!("{}", to_json(data)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(data)?);
println!("{}", to_yaml(data)?);
}
OutputFormat::Pretty => {
// This should not be called - pretty printing is handled in ui.rs
@@ -23,3 +45,121 @@ pub fn print_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<()>
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestData {
name: String,
count: i32,
active: bool,
}
#[test]
fn test_output_format_from_str_json() {
assert_eq!(OutputFormat::from_str("json"), Some(OutputFormat::Json));
assert_eq!(OutputFormat::from_str("JSON"), Some(OutputFormat::Json));
assert_eq!(OutputFormat::from_str("Json"), Some(OutputFormat::Json));
}
#[test]
fn test_output_format_from_str_yaml() {
assert_eq!(OutputFormat::from_str("yaml"), Some(OutputFormat::Yaml));
assert_eq!(OutputFormat::from_str("YAML"), Some(OutputFormat::Yaml));
assert_eq!(OutputFormat::from_str("yml"), Some(OutputFormat::Yaml));
assert_eq!(OutputFormat::from_str("YML"), Some(OutputFormat::Yaml));
}
#[test]
fn test_output_format_from_str_pretty() {
assert_eq!(OutputFormat::from_str("pretty"), Some(OutputFormat::Pretty));
assert_eq!(OutputFormat::from_str("PRETTY"), Some(OutputFormat::Pretty));
assert_eq!(OutputFormat::from_str("text"), Some(OutputFormat::Pretty));
}
#[test]
fn test_output_format_from_str_invalid() {
assert_eq!(OutputFormat::from_str("xml"), None);
assert_eq!(OutputFormat::from_str("csv"), None);
assert_eq!(OutputFormat::from_str(""), None);
}
#[test]
fn test_to_json() {
let data = TestData {
name: "test".to_string(),
count: 42,
active: true,
};
let json = to_json(&data).unwrap();
assert!(json.contains("\"name\": \"test\""));
assert!(json.contains("\"count\": 42"));
assert!(json.contains("\"active\": true"));
}
#[test]
fn test_to_yaml() {
let data = TestData {
name: "test".to_string(),
count: 42,
active: true,
};
let yaml = to_yaml(&data).unwrap();
assert!(yaml.contains("name: test"));
assert!(yaml.contains("count: 42"));
assert!(yaml.contains("active: true"));
}
#[test]
fn test_to_json_array() {
let data = vec![
TestData { name: "a".to_string(), count: 1, active: true },
TestData { name: "b".to_string(), count: 2, active: false },
];
let json = to_json(&data).unwrap();
assert!(json.contains("\"name\": \"a\""));
assert!(json.contains("\"name\": \"b\""));
}
#[test]
fn test_to_yaml_array() {
let data = vec![
TestData { name: "a".to_string(), count: 1, active: true },
TestData { name: "b".to_string(), count: 2, active: false },
];
let yaml = to_yaml(&data).unwrap();
assert!(yaml.contains("name: a"));
assert!(yaml.contains("name: b"));
}
#[test]
fn test_output_format_equality() {
assert_eq!(OutputFormat::Json, OutputFormat::Json);
assert_ne!(OutputFormat::Json, OutputFormat::Yaml);
assert_ne!(OutputFormat::Yaml, OutputFormat::Pretty);
}
#[test]
fn test_output_format_clone() {
let format = OutputFormat::Json;
let cloned = format.clone();
assert_eq!(format, cloned);
}
#[test]
fn test_to_json_special_chars() {
let data = TestData {
name: "test\"with\\special\nchars".to_string(),
count: 0,
active: false,
};
let json = to_json(&data).unwrap();
// JSON should properly escape special characters
assert!(json.contains("\\\""));
assert!(json.contains("\\\\"));
assert!(json.contains("\\n"));
}
}
@@ -115,7 +115,6 @@ class Hindsight:
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
entities: Optional[List[Dict[str, str]]] = None,
tags: Optional[List[str]] = None,
) -> RetainResponse:
"""
Store a single memory (simplified interface).
@@ -128,14 +127,13 @@ class Hindsight:
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
entities: Optional list of entities [{"text": "...", "type": "..."}]
tags: Optional list of tags for this memory
Returns:
RetainResponse with success status
"""
return self.retain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities, "tags": tags}],
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities}],
document_id=document_id,
)
@@ -145,17 +143,15 @@ class Hindsight:
items: List[Dict[str, Any]],
document_id: Optional[str] = None,
retain_async: bool = False,
document_tags: Optional[List[str]] = None,
) -> RetainResponse:
"""
Store multiple memories in batch.
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags'
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities'
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
retain_async: If True, process asynchronously in background (default: False)
document_tags: Optional list of tags to apply to all memories in this batch
Returns:
RetainResponse with success status and item count
@@ -179,14 +175,12 @@ class Hindsight:
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
entities=entities,
tags=item.get("tags"),
)
)
request_obj = retain_request.RetainRequest(
items=memory_items,
async_=retain_async,
document_tags=document_tags,
)
return _run_async(self._memory_api.retain_memories(bank_id, request_obj))
@@ -204,8 +198,6 @@ class Hindsight:
max_entity_tokens: int = 500,
include_chunks: bool = False,
max_chunk_tokens: int = 8192,
tags: Optional[List[str]] = None,
tags_match: str = "any",
) -> RecallResponse:
"""
Recall memories using semantic similarity.
@@ -222,9 +214,6 @@ class Hindsight:
max_entity_tokens: Maximum tokens for entity observations (default: 500)
include_chunks: Include raw text chunks in results (default: False)
max_chunk_tokens: Maximum tokens for chunks (default: 8192)
tags: Optional list of tags to filter memories by
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
Returns:
RecallResponse with results, optional entities, optional chunks, and optional trace
@@ -244,8 +233,6 @@ class Hindsight:
trace=trace,
query_timestamp=query_timestamp,
include=include_opts,
tags=tags,
tags_match=tags_match,
)
return _run_async(self._memory_api.recall_memories(bank_id, request_obj))
@@ -258,8 +245,6 @@ class Hindsight:
context: Optional[str] = None,
max_tokens: Optional[int] = None,
response_schema: Optional[Dict[str, Any]] = None,
tags: Optional[List[str]] = None,
tags_match: str = "any",
) -> ReflectResponse:
"""
Generate a contextual answer based on bank identity and memories.
@@ -273,9 +258,6 @@ class Hindsight:
response_schema: Optional JSON Schema for structured output. When provided,
the response will include a 'structured_output' field with the LLM
response parsed according to this schema.
tags: Optional list of tags to filter memories by
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
Returns:
ReflectResponse with answer text, optionally facts used, and optionally
@@ -287,8 +269,6 @@ class Hindsight:
context=context,
max_tokens=max_tokens,
response_schema=response_schema,
tags=tags,
tags_match=tags_match,
)
return _run_async(self._memory_api.reflect(bank_id, request_obj))
+1 -22
View File
@@ -102,8 +102,6 @@ export class HindsightClient {
documentId?: string;
async?: boolean;
entities?: EntityInput[];
/** Optional list of tags for this memory */
tags?: string[];
}
): Promise<RetainResponse> {
const item: {
@@ -113,7 +111,6 @@ export class HindsightClient {
metadata?: Record<string, string>;
document_id?: string;
entities?: EntityInput[];
tags?: string[];
} = { content };
if (options?.timestamp) {
item.timestamp =
@@ -133,9 +130,6 @@ export class HindsightClient {
if (options?.entities) {
item.entities = options.entities;
}
if (options?.tags) {
item.tags = options.tags;
}
const response = await sdk.retainMemories({
client: this.client,
@@ -198,10 +192,6 @@ export class HindsightClient {
maxEntityTokens?: number;
includeChunks?: boolean;
maxChunkTokens?: number;
/** Optional list of tags to filter memories by */
tags?: string[];
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
}
): Promise<RecallResponse> {
const response = await sdk.recallMemories({
@@ -218,8 +208,6 @@ export class HindsightClient {
entities: options?.includeEntities ? { max_tokens: options?.maxEntityTokens ?? 500 } : undefined,
chunks: options?.includeChunks ? { max_tokens: options?.maxChunkTokens ?? 8192 } : undefined,
},
tags: options?.tags,
tags_match: options?.tagsMatch,
},
});
@@ -232,14 +220,7 @@ export class HindsightClient {
async reflect(
bankId: string,
query: string,
options?: {
context?: string;
budget?: Budget;
/** Optional list of tags to filter memories by */
tags?: string[];
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
}
options?: { context?: string; budget?: Budget }
): Promise<ReflectResponse> {
const response = await sdk.reflect({
client: this.client,
@@ -248,8 +229,6 @@ export class HindsightClient {
query,
context: options?.context,
budget: options?.budget || 'low',
tags: options?.tags,
tags_match: options?.tagsMatch,
},
});
+1 -51
View File
@@ -43,13 +43,11 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|-----------|------|---------|-------------|
| `query` | string | required | Natural language query |
| `types` | list | all | Filter: `world`, `experience`, `opinion` |
| `budget` | string | "mid" | Budget level: `low`, `mid`, `high` |
| `budget` | string | "mid" | Budget level: "low", "mid", "high" |
| `max_tokens` | int | 4096 | Token budget for results |
| `trace` | bool | false | Enable trace output for debugging |
| `include_entities` | bool | false | Include entity observations |
| `max_entity_tokens` | int | 500 | Token budget for entity observations |
| `tags` | list | None | Filter memories by tags (see [Tag Filtering](#filter-by-tags)) |
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
<Tabs>
<TabItem value="python" label="Python">
@@ -129,51 +127,3 @@ The `budget` parameter controls graph traversal depth:
<CodeSnippet code={recallMjs} section="recall-budget-levels" language="javascript" />
</TabItem>
</Tabs>
## Filter by Tags
Tags enable **visibility scoping**—filter memories based on tags assigned during [retain](./retain#tagging-memories). This is essential for multi-user agents where each user should only see their own memories.
### Basic Tag Filtering
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-with-tags" language="python" />
</TabItem>
</Tabs>
### Tag Match Modes
The `tags_match` parameter controls how tags are matched:
| Mode | Behavior | Untagged Memories |
|------|----------|-------------------|
| `any` | OR: memory has ANY of the specified tags | **Included** |
| `all` | AND: memory has ALL of the specified tags | **Included** |
| `any_strict` | OR: memory has ANY of the specified tags | **Excluded** |
| `all_strict` | AND: memory has ALL of the specified tags | **Excluded** |
**Strict modes** are useful when you want to ensure only tagged memories are returned:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-tags-strict" language="python" />
</TabItem>
</Tabs>
**AND matching** requires all specified tags to be present:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-tags-all" language="python" />
</TabItem>
</Tabs>
### Use Cases
| Scenario | Tags | Mode | Result |
|----------|------|------|--------|
| User A's memories only | `["user:alice"]` | `any_strict` | Only memories tagged `user:alice` |
| Support + feedback | `["support", "feedback"]` | `any` | Memories with either tag + untagged |
| Multi-user room | `["user:alice", "room:general"]` | `all_strict` | Only memories with both tags |
| Global + user-specific | `["user:alice"]` | `any` | Alice's memories + shared (untagged) |
+1 -24
View File
@@ -50,12 +50,10 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | string | required | Question or prompt |
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` |
| `budget` | string | "low" | Budget level: "low", "mid", "high" |
| `context` | string | None | Additional context for the query |
| `max_tokens` | int | 4096 | Maximum tokens for the response |
| `response_schema` | object | None | JSON Schema for [structured output](#structured-output) |
| `tags` | list | None | Filter memories by tags during reflection |
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
### Response Fields
@@ -249,24 +247,3 @@ hindsight memory reflect hiring-team \
- Use `model_validate()` to parse the response back into your Pydantic model
- Keep schemas focused — extract only what you need
- Use `Optional` fields for data that may not always be available
## Filter by Tags
Like [recall](./recall#filter-by-tags), reflect supports tag filtering to scope which memories are considered during reasoning. This is essential for multi-user scenarios where reflection should only consider memories relevant to a specific user.
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectPy} section="reflect-with-tags" language="python" />
</TabItem>
</Tabs>
The `tags_match` parameter works the same as in recall:
| Mode | Behavior |
|------|----------|
| `any` | OR matching, includes untagged memories |
| `all` | AND matching, includes untagged memories |
| `any_strict` | OR matching, excludes untagged memories |
| `all_strict` | AND matching, excludes untagged memories |
See [Retain API](./retain#tagging-memories) for how to tag memories and [Recall API](./recall#filter-by-tags) for more details on tag matching modes.
@@ -129,55 +129,3 @@ For large batches, use async ingestion to avoid blocking:
<CodeSnippet code={retainMjs} section="retain-async" language="javascript" />
</TabItem>
</Tabs>
## Tagging Memories
Tags enable **visibility scoping**—useful when one memory bank serves multiple users but each should only see relevant memories. For example, an agent that chats with multiple users can tag memories by user ID and filter during recall.
### Tag Individual Items
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={retainPy} section="retain-with-tags" language="python" />
</TabItem>
</Tabs>
### Apply Tags to All Items in a Batch
Use `document_tags` to apply the same tags to all items in a request:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={retainPy} section="retain-with-document-tags" language="python" />
</TabItem>
</Tabs>
When both `document_tags` and item-level `tags` are provided, they are merged together.
### Tag Naming Conventions
Use consistent naming patterns for tags:
| Pattern | Example | Use Case |
|---------|---------|----------|
| `user:<id>` | `user:alice` | Multi-user agent filtering |
| `session:<id>` | `session:123` | Session-based scoping |
| `room:<id>` | `room:general` | Chat room isolation |
| `topic:<name>` | `topic:feedback` | Topic categorization |
### Listing Tags
Use the list tags API to discover existing tags, useful for UI autocomplete or wildcard expansion:
```python
# List all tags in a bank
tags = client.list_tags(bank_id="my-bank")
for tag in tags.items:
print(f"{tag.tag}: {tag.count} memories")
# Search with wildcards (* matches any characters)
user_tags = client.list_tags(bank_id="my-bank", q="user:*")
admin_tags = client.list_tags(bank_id="my-bank", q="*-admin")
```
See [Recall API](./recall#filter-by-tags) for filtering memories by tags during retrieval.
+12 -102
View File
@@ -95,71 +95,29 @@ Converts text into dense vector representations for semantic similarity search.
**Default:** `BAAI/bge-small-en-v1.5` (384 dimensions, ~130MB)
### Supported Providers
**Alternatives:**
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers (default) | Development, low latency |
| `openai` | OpenAI embeddings API | Production, high quality |
| `cohere` | Cohere embeddings API | Production, multilingual |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| Model | Use Case |
|-------|----------|
| `BAAI/bge-small-en-v1.5` | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | Multilingual (50+ languages) |
### Local Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
### OpenAI Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `text-embedding-3-small` | 1536 | Default OpenAI, cost-effective |
| `text-embedding-3-large` | 3072 | Higher quality, more expensive |
| `text-embedding-ada-002` | 1536 | Legacy model |
### Cohere Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `embed-english-v3.0` | 1024 | English text |
| `embed-multilingual-v3.0` | 1024 | 100+ languages |
:::warning Embedding Dimensions
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
:::warning
All embedding models must produce **384-dimensional vectors** to match the database schema.
:::
**Configuration Examples:**
**Configuration:**
```bash
# Local provider (default)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# OpenAI
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
# Cohere
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
# TEI (self-hosted)
# TEI provider (remote)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
# LiteLLM proxy
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small
```
See [Configuration](./configuration#embeddings) for all options including Azure OpenAI and custom endpoints.
---
## Cross-Encoder (Reranker)
@@ -168,18 +126,7 @@ Reranks initial search results to improve precision.
**Default:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (~85MB)
### Supported Providers
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers CrossEncoder (default) | Development, low latency |
| `cohere` | Cohere rerank API | Production, high quality |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `flashrank` | FlashRank (lightweight, fast) | Resource-constrained environments |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| `rrf` | RRF-only (no neural reranking) | Testing, minimal resources |
### Local Models
**Alternatives:**
| Model | Use Case |
|-------|----------|
@@ -187,51 +134,14 @@ Reranks initial search results to improve precision.
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
### Cohere Models
| Model | Use Case |
|-------|----------|
| `rerank-english-v3.0` | English text |
| `rerank-multilingual-v3.0` | 100+ languages |
### LiteLLM Supported Providers
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
| Provider | Model Example |
|----------|---------------|
| Cohere | `cohere/rerank-english-v3.0` |
| Together AI | `together_ai/...` |
| Voyage AI | `voyage/rerank-2` |
| Jina AI | `jina_ai/...` |
| AWS Bedrock | `bedrock/...` |
**Configuration Examples:**
**Configuration:**
```bash
# Local provider (default)
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Cohere
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# TEI (self-hosted)
# TEI provider (remote)
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
# FlashRank (lightweight)
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
# LiteLLM proxy
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0
# RRF-only (no neural reranking)
export HINDSIGHT_API_RERANKER_PROVIDER=rrf
```
See [Configuration](./configuration#reranker) for all options including Azure-hosted endpoints and batch settings.
+1 -1
View File
@@ -183,4 +183,4 @@ Disposition creates **consistent character** across conversations while allowing
- [**Retain**](./retain) — How rich facts are stored
- [**Recall**](./retrieval) — How multi-strategy search works
- [**Reflect API**](./api/reflect) — Code examples, parameters, and tag filtering
- [API Reference: Reflect](./api/reflect) — Code examples and usage
+27 -6
View File
@@ -169,13 +169,34 @@ As facts accumulate about an entity, Hindsight synthesizes **observations** —
## Tagging Memories
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
You can tag memories for filtering during recall—useful when one memory bank serves multiple users but each user should only see relevant memories.
- **Item tags**: Tag individual memories with specific scopes
- **Document tags**: Apply tags to all items in a batch
- **Tag filtering**: Filter during recall/reflect by tags
```python
# Tag memories for specific users
client.retain(
bank_id="my-agent",
items=[
{
"content": "Alice prefers morning meetings",
"tags": ["user_alice"]
}
]
)
See [Retain API](./api/retain) for code examples and [Recall API](./api/recall) for filtering options.
# Apply tags to all items in a batch
client.retain(
bank_id="my-agent",
document_tags=["session_123", "user_alice"], # Applied to all items
items=[
{"content": "Alice discussed the project timeline"},
{"content": "Alice mentioned she needs help with Python"}
]
)
```
During recall, use `tags_match` to control matching:
- `"any"` (default): OR matching - returns memories where **any** tag overlaps
- `"all"`: AND matching - returns memories containing **all** specified tags
---
@@ -198,4 +219,4 @@ All stored in your isolated **memory bank**, ready for `recall()` and `reflect()
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
- [**Reflect**](./reflect) — How disposition influences reasoning and opinion formation
- [**Retain API**](./api/retain) — Code examples and parameters
- [API Reference](./api/retain) — Code examples for retaining memories
+3 -4
View File
@@ -133,9 +133,9 @@ Hindsight is built for AI agents, not humans. Traditional search systems return
**Parameters you control:**
- `max_tokens`: How much memory content to return (default: 4096 tokens)
- `budget`: Search depth level (low, mid, high)
- `types`: Filter by world, experience, opinion, or all
- `tags`: Filter memories by visibility tags
- `tags_match`: How to match tags (see [Recall API](./api/recall) for all options)
- `fact_type`: Filter by world, experience, opinion, or all
- `tags`: Filter memories by tags
- `tags_match`: How to match tags - `"any"` for OR (default), `"all"` for AND
### Expanding Context: Chunks and Entity Observations
@@ -243,4 +243,3 @@ See [Configuration → Retrieval](./configuration#retrieval) for available algor
- [**Retain**](./retain) — How memories are stored with rich context
- [**Reflect**](./reflect) — How disposition influences reasoning
- [**Recall API**](./api/recall) — Code examples, parameters, and tag filtering
+187 -7
View File
@@ -14,6 +14,15 @@ Hindsight provides an Agent Skill that gives AI coding assistants persistent mem
| [OpenCode](https://github.com/opencode-ai/opencode) | `~/.opencode/skills/` |
| [Codex CLI](https://github.com/openai/codex) | `~/.codex/skills/` |
## Deployment Modes
The skill supports two deployment modes:
| Mode | Best For | Data Location |
|------|----------|---------------|
| **Local** | Individual developers | Your machine (`~/.pg0/`) |
| **Cloud** | Teams sharing knowledge | Hindsight Cloud |
## Quick Install
```bash
@@ -22,13 +31,14 @@ curl -fsSL https://hindsight.vectorize.io/get-skill | bash
The installer will:
1. Prompt you to select your AI coding assistant
2. Run the LLM provider configuration
3. Install the skill to the appropriate directory
2. Select deployment mode (local or cloud)
3. Configure the appropriate settings
4. Install the skill to the appropriate directory
### Install for a Specific Platform
```bash
# Claude Code
# Claude Code (interactive mode selection)
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude
# OpenCode
@@ -38,6 +48,13 @@ curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app opencode
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app codex
```
### Install with Cloud Mode
```bash
# Direct cloud setup (skips interactive prompts for mode)
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude --mode cloud
```
## What the Skill Provides
Once installed, your AI assistant gains the ability to:
@@ -68,6 +85,8 @@ The skill is optimized to store:
## Architecture
### Local Mode
```
AI Coding Assistant
@@ -86,7 +105,29 @@ Embedded PostgreSQL (~/.pg0/hindsight-embed/)
All data stays on your machine. The daemon auto-starts when needed and shuts down after inactivity.
## Configuration
### Cloud Mode
```
AI Coding Assistant
Hindsight Skill (SKILL.md)
hindsight-cli
Hindsight Cloud API (https://api.hindsight.vectorize.io)
Shared Memory Bank (team-accessible)
```
Data is stored in Hindsight Cloud and shared across your team. All team members with the same bank ID can access shared memories.
---
## Local Mode Setup
The skill uses configuration stored in `~/.hindsight/config.env`. Reconfigure anytime:
@@ -94,6 +135,110 @@ The skill uses configuration stored in `~/.hindsight/config.env`. Reconfigure an
uvx hindsight-embed configure
```
---
## Cloud Mode Setup
Cloud mode connects to [Hindsight Cloud](https://vectorize.io/hindsight/cloud), allowing teams to share memories about a codebase. When one team member learns something, everyone benefits.
### Prerequisites
1. A Hindsight Cloud account ([request access](https://vectorize.io/hindsight/cloud))
2. An API key from your team admin
3. A bank ID for your project (e.g., `team-acme-frontend`)
### Installation
Run the installer with cloud mode:
```bash
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
```
You'll be prompted for:
| Setting | Description | Example |
|---------|-------------|---------|
| **Cloud API URL** | Hindsight Cloud endpoint | `https://api.hindsight.vectorize.io` |
| **API Key** | Your authentication key | `hs_xxx...` |
| **Bank ID** | Shared memory bank for your team | `team-acme-frontend` |
### Configuration Files
Cloud mode creates two files:
**`~/.hindsight/config`** — API connection settings (TOML format):
```toml
api_url = "https://api.hindsight.vectorize.io"
api_key = "hs_xxx..."
```
**`~/.claude/skills/hindsight/SKILL.md`** — Skill definition with your bank ID baked in.
### Team Setup
To set up cloud mode for your team:
1. **Team admin** creates a bank in Hindsight Cloud (e.g., `team-acme-frontend`)
2. **Team admin** generates API keys for each team member
3. **Each developer** runs the installer with their API key and the shared bank ID
4. All team members now share the same memory bank
### What to Store in Team Banks
Cloud mode uses a **shared team bank**. Be thoughtful about what goes in:
| Type | Examples | How to Store |
|------|----------|--------------|
| **Project conventions** | Linting rules, testing requirements, Node version | `"Project uses ESLint with Airbnb config"` |
| **Team knowledge** | Architecture decisions, common pitfalls, domain logic | `"Auth module requires Redis 7+"` |
| **Individual preferences** | Personal coding style, communication preferences | `"Alice prefers verbose commit messages"` |
**Key distinction**: Project conventions apply to everyone. Individual preferences should include the person's name so the AI knows when to apply them.
### Example Workflow
```
Day 1: Alice discovers a requirement
─────────────────────────────────────
Alice's AI assistant stores:
"The auth module requires Redis 7+ due to HEXPIRE command usage"
"Alice prefers explicit error handling over silent failures"
Day 2: Bob starts working on auth
─────────────────────────────────
Bob's AI assistant recalls:
"The auth module requires Redis 7+ due to HEXPIRE command usage"
Bob avoids the same issue Alice hit!
(Alice's personal preference is stored but won't be applied to Bob)
```
### Testing Cloud Connection
After installation, verify the connection:
```bash
# Store a test memory
hindsight memory retain team-acme-frontend "Alice works at Google as a software engineer"
# Recall it
hindsight memory recall team-acme-frontend "Alice"
```
### Switching Between Banks
If you work on multiple projects, you can have different skills installed for each AI assistant, or manually switch banks:
```bash
# Environment variable override (temporary)
HINDSIGHT_API_URL=https://api.hindsight.vectorize.io \
HINDSIGHT_API_KEY=hs_xxx \
hindsight memory recall different-bank "query"
```
For permanent multi-bank setups, reinstall the skill with a different bank ID.
## Troubleshooting
### Skill not activating
@@ -102,20 +247,55 @@ The skill activates based on its description matching your request. Try being ex
- "Remember that..." triggers storage
- "What do you know about..." triggers recall
### Daemon issues
### Local Mode Issues
**Daemon not starting:**
```bash
uvx hindsight-embed daemon status
uvx hindsight-embed daemon logs
```
### Reconfigure
**Reconfigure LLM provider:**
```bash
uvx hindsight-embed configure
```
### Cloud Mode Issues
**Authentication errors:**
```bash
# Verify your config
cat ~/.hindsight/config
# Test connection manually
hindsight bank list
```
**Wrong bank ID:**
Check your SKILL.md file to see which bank ID is configured:
```bash
cat ~/.claude/skills/hindsight/SKILL.md | grep "memory retain"
```
To change the bank ID, reinstall the skill:
```bash
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
```
**Network/firewall issues:**
```bash
# Test connectivity to cloud API
curl -I https://api.hindsight.vectorize.io/health
```
## Requirements
### Local Mode
- Python 3.10+ (for `uvx`)
- An LLM API key (OpenAI, Anthropic, Groq, etc.)
### Cloud Mode
- Python 3.10+ (for `uvx`)
- Hindsight Cloud API key
- Network access to `https://api.hindsight.vectorize.io`
-33
View File
@@ -116,39 +116,6 @@ results = client.recall(bank_id="my-bank", query="How are Alice and Bob connecte
# [/docs:recall-budget-levels]
# [docs:recall-with-tags]
# Filter recall to only memories tagged for a specific user
response = client.recall(
bank_id="my-bank",
query="What feedback did the user give?",
tags=["user:alice"],
tags_match="any" # OR matching, includes untagged (default)
)
# [/docs:recall-with-tags]
# [docs:recall-tags-strict]
# Strict mode: only return memories that have matching tags (exclude untagged)
response = client.recall(
bank_id="my-bank",
query="What did the user say?",
tags=["user:alice"],
tags_match="any_strict" # OR matching, excludes untagged memories
)
# [/docs:recall-tags-strict]
# [docs:recall-tags-all]
# AND matching: require ALL specified tags to be present
response = client.recall(
bank_id="my-bank",
query="What bugs were reported?",
tags=["user:alice", "bug-report"],
tags_match="all_strict" # Memory must have BOTH tags
)
# [/docs:recall-tags-all]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
-11
View File
@@ -81,17 +81,6 @@ for fact in response.based_on or []:
# [/docs:reflect-sources]
# [docs:reflect-with-tags]
# Filter reflection to only consider memories for a specific user
response = client.reflect(
bank_id="my-bank",
query="What does this user think about our product?",
tags=["user:alice"],
tags_match="any_strict" # Only use memories tagged for this user
)
# [/docs:reflect-with-tags]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
-33
View File
@@ -67,39 +67,6 @@ print(result.var_async) # True
# [/docs:retain-async]
# [docs:retain-with-tags]
# Tag individual items for visibility scoping
client.retain_batch(
bank_id="my-bank",
items=[
{
"content": "User Alice said she loves the new dashboard",
"tags": ["user:alice", "feedback"]
},
{
"content": "User Bob reported a bug in the search feature",
"tags": ["user:bob", "bug-report"]
}
],
document_id="user_feedback_001"
)
# [/docs:retain-with-tags]
# [docs:retain-with-document-tags]
# Apply tags to all items in a batch
client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Alice mentioned she prefers dark mode"},
{"content": "Bob asked about keyboard shortcuts"}
],
document_id="support_session_123",
document_tags=["session:123", "support"] # Applied to all items
)
# [/docs:retain-with-document-tags]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
+277 -41
View File
@@ -6,11 +6,12 @@
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash
#
# Options:
# --app <app> Target app: claude, opencode, codex
# --app <app> Target app: claude, opencode, codex
# --mode <mode> Mode: local (default) or cloud
#
# Examples:
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app opencode
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude --mode cloud
#
set -e
@@ -62,8 +63,8 @@ print_banner() {
echo ""
}
# Embedded SKILL.md content
SKILL_CONTENT='---
# Embedded SKILL.md content for LOCAL mode (uses hindsight-embed with local daemon)
SKILL_CONTENT_LOCAL='---
name: hindsight
description: Store user preferences, learnings from tasks, and procedure outcomes. Use to remember what works and recall context before new tasks.
---
@@ -139,6 +140,107 @@ uvx hindsight-embed memory reflect default "How should I approach this task base
4. **Recall first**: Always check for relevant context before starting work
'
# Template for CLOUD mode SKILL.md (uses hindsight-cli directly with remote API)
# The BANK_ID placeholder will be replaced during installation
SKILL_CONTENT_CLOUD_TEMPLATE='---
name: hindsight
description: Store team knowledge, project conventions, and learnings from tasks. Use to remember what works and recall context before new tasks. This is a shared team memory bank.
---
# Hindsight Memory Skill (Cloud)
You have persistent memory via **Hindsight Cloud**. This memory bank is **shared with the team**, so knowledge stored here benefits everyone working on this codebase.
**Proactively store team knowledge and recall context** to provide better assistance.
## Commands
### Store a memory
Use `memory retain` to store what you learn:
```bash
hindsight memory retain BANK_ID "Project uses ESLint with Airbnb config and Prettier for formatting"
hindsight memory retain BANK_ID "Running tests requires NODE_ENV=test" --context procedures
hindsight memory retain BANK_ID "Build failed when using Node 18, works with Node 20" --context learnings
hindsight memory retain BANK_ID "Alice prefers verbose commit messages with context" --context preferences
```
### Recall memories
Use `memory recall` BEFORE starting tasks to get relevant context:
```bash
hindsight memory recall BANK_ID "project conventions and coding standards"
hindsight memory recall BANK_ID "Alice preferences for this project"
hindsight memory recall BANK_ID "what issues have we encountered before"
hindsight memory recall BANK_ID "how does the auth module work"
```
### Reflect on memories
Use `memory reflect` to synthesize context:
```bash
hindsight memory reflect BANK_ID "How should I approach this task based on past experience?"
```
## IMPORTANT: When to Store Memories
This is a **shared team bank**. Store knowledge that benefits the team. For individual preferences, include the persons name.
### Project/Team Conventions (shared)
- Coding standards ("Project uses 2-space indentation")
- Required tools and versions ("Project requires Node 20+, PostgreSQL 15+")
- Linting and formatting rules ("ESLint with Airbnb config")
- Testing conventions ("Integration tests require Docker running")
- Branch naming and PR conventions
### Individual Preferences (attribute to person)
- Personal coding style ("Alice prefers explicit type annotations")
- Communication preferences ("Bob prefers detailed PR descriptions")
- Tool preferences ("Carol uses vim keybindings")
### Procedure Outcomes
- Steps that successfully completed a task
- Commands that worked (or failed) and why
- Workarounds discovered
- Configuration that resolved issues
### Learnings from Tasks
- Bugs encountered and their solutions
- Performance optimizations that worked
- Architecture decisions and rationale
- Dependencies or version requirements
### Team Knowledge
- Onboarding information for new team members
- Common pitfalls and how to avoid them
- Architecture decisions and their rationale
- Integration points with external systems
- Domain knowledge and business logic explanations
## IMPORTANT: When to Recall Memories
**Always recall** before:
- Starting any non-trivial task
- Making decisions about implementation
- Suggesting tools, libraries, or approaches
- Writing code in a new area of the project
- When answering questions about the codebase
- When a team member asks how something works
## Best Practices
1. **Store immediately**: When you discover something, store it right away
2. **Be specific**: Store "npm test requires --experimental-vm-modules flag" not "tests need a flag"
3. **Include outcomes**: Store what worked AND what did not work
4. **Recall first**: Always check for relevant context before starting work
5. **Think team-first**: Store knowledge that would help other team members
6. **Attribute individual preferences**: Store "Alice prefers X" not just "User prefers X"
7. **Distinguish project vs personal**: Project conventions apply to everyone; personal preferences are per-person
'
# Get skills directory for app (bash 3.x compatible)
get_skills_dir() {
case "$1" in
@@ -161,16 +263,18 @@ get_app_name() {
# Parse arguments
APP=""
MODE=""
show_usage() {
echo "Usage: $0 [--app <app>]"
echo "Usage: $0 [--app <app>] [--mode <mode>]"
echo ""
echo "Options:"
echo " --app <app> Target app: claude, opencode, codex"
echo " --app <app> Target app: claude, opencode, codex"
echo " --mode <mode> Mode: local (default) or cloud"
echo ""
echo "Examples:"
echo " $0 --app claude"
echo " $0 --app opencode"
echo " $0 --app claude --mode cloud"
exit 1
}
@@ -180,6 +284,10 @@ while [[ $# -gt 0 ]]; do
APP="$2"
shift 2
;;
--mode)
MODE="$2"
shift 2
;;
--help|-h)
show_usage
;;
@@ -233,6 +341,41 @@ fi
print_info "Installing for ${BOLD}$APP_NAME${NC}"
# Select mode (local vs cloud)
if [ -z "$MODE" ]; then
if [ -t 0 ] || [ -e /dev/tty ]; then
echo ""
echo -e "${DIM}Select deployment mode:${NC}"
echo ""
echo -e " ${BOLD}1)${NC} Local ${DIM}- Run Hindsight on your machine (default)${NC}"
echo -e " ${BOLD}2)${NC} Cloud ${DIM}- Connect to Hindsight Cloud (for teams)${NC}"
echo ""
if [ -t 0 ]; then
read -p "Enter choice [1]: " mode_choice
else
read -p "Enter choice [1]: " mode_choice </dev/tty
fi
mode_choice=${mode_choice:-1}
case $mode_choice in
1) MODE="local" ;;
2) MODE="cloud" ;;
*) MODE="local" ;;
esac
echo ""
else
MODE="local"
print_info "Non-interactive mode detected, defaulting to local"
fi
fi
# Validate mode
if [ "$MODE" != "local" ] && [ "$MODE" != "cloud" ]; then
print_error "Unknown mode '$MODE'. Supported: local, cloud"
fi
print_info "Mode: ${BOLD}$MODE${NC}"
# Step 1: Check for Python/uvx
print_step "Checking prerequisites"
@@ -241,40 +384,133 @@ if ! command -v python3 &> /dev/null && ! command -v uvx &> /dev/null; then
fi
print_success "Python/uvx available"
# Step 2: Configure LLM provider using the CLI
print_step "Configuring LLM provider"
# Configuration depends on mode
if [ "$MODE" = "local" ]; then
# LOCAL MODE: Configure LLM provider using hindsight-embed
print_step "Configuring LLM provider"
# Install/run hindsight-embed configure
# Redirect stdin from /dev/tty to avoid "not a terminal" warnings
if command -v uvx &> /dev/null; then
uvx hindsight-embed configure </dev/tty
else
pip install -q hindsight-embed
hindsight-embed configure </dev/tty
fi
# Install local skill
print_step "Installing skill to $APP_NAME"
mkdir -p "$SKILLS_DIR/hindsight"
echo "$SKILL_CONTENT_LOCAL" > "$SKILLS_DIR/hindsight/SKILL.md"
print_success "Installed to $SKILLS_DIR/hindsight/"
# Done (local)!
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ✓ Installation Complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " The Hindsight skill is now available in ${BOLD}$APP_NAME${NC}."
echo ""
echo -e " ${DIM}Test the CLI:${NC}"
echo -e " ${CYAN}uvx hindsight-embed memory retain default \"Alice works at Google as a software engineer\"${NC}"
echo -e " ${CYAN}uvx hindsight-embed memory recall default \"Alice\"${NC}"
echo ""
echo -e " ${DIM}$APP_NAME will automatically use the skill when relevant.${NC}"
echo ""
echo -e " ${DIM}Documentation:${NC} ${BLUE}https://hindsight.vectorize.io${NC}"
echo ""
# Install/run hindsight-embed configure
# Redirect stdin from /dev/tty to avoid "not a terminal" warnings
if command -v uvx &> /dev/null; then
uvx hindsight-embed configure </dev/tty
else
pip install -q hindsight-embed
hindsight-embed configure </dev/tty
# CLOUD MODE: Install CLI and configure connection to Hindsight Cloud
print_step "Installing Hindsight CLI"
# Download and install the CLI binary
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
print_step "Configuring Hindsight Cloud connection"
DEFAULT_CLOUD_URL="https://api.hindsight.vectorize.io"
echo -e "${DIM}Enter your Hindsight Cloud connection details.${NC}"
echo -e "${DIM}Get these from your team admin or https://ui.hindsight.vectorize.io${NC}"
echo ""
# Prompt for cloud URL
if [ -t 0 ]; then
read -p "Cloud API URL [$DEFAULT_CLOUD_URL]: " CLOUD_URL
else
read -p "Cloud API URL [$DEFAULT_CLOUD_URL]: " CLOUD_URL </dev/tty
fi
CLOUD_URL=${CLOUD_URL:-$DEFAULT_CLOUD_URL}
# Prompt for API key
if [ -t 0 ]; then
read -p "API Key: " CLOUD_API_KEY
else
read -p "API Key: " CLOUD_API_KEY </dev/tty
fi
if [ -z "$CLOUD_API_KEY" ]; then
print_error "API Key is required for cloud mode"
fi
# Prompt for bank ID
if [ -t 0 ]; then
read -p "Bank ID (e.g., team-myproject): " CLOUD_BANK_ID
else
read -p "Bank ID (e.g., team-myproject): " CLOUD_BANK_ID </dev/tty
fi
if [ -z "$CLOUD_BANK_ID" ]; then
print_error "Bank ID is required for cloud mode"
fi
# Save config to ~/.hindsight/config
print_step "Saving configuration"
HINDSIGHT_CONFIG_DIR="$HOME/.hindsight"
HINDSIGHT_CONFIG_FILE="$HINDSIGHT_CONFIG_DIR/config"
mkdir -p "$HINDSIGHT_CONFIG_DIR"
# Write config file (TOML format for hindsight-cli)
cat > "$HINDSIGHT_CONFIG_FILE" << EOF
api_url = "$CLOUD_URL"
api_key = "$CLOUD_API_KEY"
EOF
# Set restrictive permissions on config file (contains API key)
chmod 600 "$HINDSIGHT_CONFIG_FILE"
print_success "Saved to $HINDSIGHT_CONFIG_FILE"
# Install cloud skill with bank ID substituted
print_step "Installing skill to $APP_NAME"
mkdir -p "$SKILLS_DIR/hindsight"
# Replace BANK_ID placeholder with actual bank ID
SKILL_CONTENT_CLOUD=$(echo "$SKILL_CONTENT_CLOUD_TEMPLATE" | sed "s/BANK_ID/$CLOUD_BANK_ID/g")
echo "$SKILL_CONTENT_CLOUD" > "$SKILLS_DIR/hindsight/SKILL.md"
print_success "Installed to $SKILLS_DIR/hindsight/"
# Done (cloud)!
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ✓ Installation Complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " The Hindsight skill is now available in ${BOLD}$APP_NAME${NC}."
echo -e " Connected to: ${CYAN}$CLOUD_URL${NC}"
echo -e " Memory bank: ${CYAN}$CLOUD_BANK_ID${NC}"
echo ""
echo -e " ${DIM}Test the CLI:${NC}"
echo -e " ${CYAN}hindsight memory retain $CLOUD_BANK_ID \"Alice works at Google as a software engineer\"${NC}"
echo -e " ${CYAN}hindsight memory recall $CLOUD_BANK_ID \"Alice\"${NC}"
echo ""
echo -e " ${DIM}Share this bank ID with your team for shared memories.${NC}"
echo -e " ${DIM}$APP_NAME will automatically use the skill when relevant.${NC}"
echo ""
echo -e " ${DIM}Documentation:${NC} ${BLUE}https://hindsight.vectorize.io${NC}"
echo ""
fi
# Step 3: Install skill to app's skills directory
print_step "Installing skill to $APP_NAME"
mkdir -p "$SKILLS_DIR/hindsight"
# Write embedded SKILL.md content
echo "$SKILL_CONTENT" > "$SKILLS_DIR/hindsight/SKILL.md"
print_success "Installed to $SKILLS_DIR/hindsight/"
# Done!
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ✓ Installation Complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " The Hindsight skill is now available in ${BOLD}$APP_NAME${NC}."
echo ""
echo -e " ${DIM}Test the CLI:${NC}"
echo -e " ${CYAN}uvx hindsight-embed memory retain default \"Test memory\"${NC}"
echo -e " ${CYAN}uvx hindsight-embed memory recall default \"test\"${NC}"
echo ""
echo -e " ${DIM}$APP_NAME will automatically use the skill when relevant.${NC}"
echo ""
echo -e " ${DIM}Documentation:${NC} ${BLUE}https://hindsight.vectorize.io${NC}"
echo ""
+2 -2
View File
@@ -353,8 +353,8 @@ def _do_configure_interactive():
print(f" \033[2mConfig:\033[0m {CONFIG_FILE}")
print()
print(" \033[2mTest with:\033[0m")
print(' \033[36mhindsight-embed retain "Test memory"\033[0m')
print(' \033[36mhindsight-embed recall "test"\033[0m')
print(' \033[36mhindsight-embed retain "Alice works at Google as a software engineer"\033[0m')
print(' \033[36mhindsight-embed recall "Alice"\033[0m')
print()
return 0