2026-01-26 01:31:58 -06:00
//! Vestige CLI
//!
//! Command-line interface for managing cognitive memory system.
2026-02-12 03:02:32 -06:00
use std ::io ::{ BufWriter , Write } ;
2026-01-26 01:31:58 -06:00
use std ::path ::PathBuf ;
2026-02-12 03:02:32 -06:00
use chrono ::{ NaiveDate , Utc } ;
2026-01-26 01:31:58 -06:00
use clap ::{ Parser , Subcommand } ;
use colored ::Colorize ;
2026-02-12 03:02:32 -06:00
use directories ::ProjectDirs ;
2026-01-26 01:31:58 -06:00
use vestige_core ::{ IngestInput , Storage } ;
/// Vestige - Cognitive Memory System CLI
#[ derive(Parser) ]
#[ command(name = " vestige " ) ]
#[ command(author = " samvallad33 " ) ]
2026-01-26 02:06:22 -06:00
#[ command(version = env!( " CARGO_PKG_VERSION " )) ]
2026-01-26 01:31:58 -06:00
#[ command(about = " CLI for the Vestige cognitive memory system " ) ]
#[ command(long_about = " Vestige is a cognitive memory system based on 130 years of memory research. \n \n It implements FSRS-6, spreading activation, synaptic tagging, and more. " ) ]
struct Cli {
#[ command(subcommand) ]
command : Commands ,
}
#[ derive(Subcommand) ]
enum Commands {
/// Show memory statistics
Stats {
/// Show tagging/retention distribution
#[ arg(long) ]
tagging : bool ,
/// Show cognitive state distribution
#[ arg(long) ]
states : bool ,
} ,
/// Run health check with warnings and recommendations
Health ,
/// Run memory consolidation cycle
Consolidate ,
/// Restore memories from backup file
Restore {
/// Path to backup JSON file
file : PathBuf ,
} ,
2026-02-12 03:02:32 -06:00
/// Create a full backup of the SQLite database
Backup {
/// Output file path for the backup
output : PathBuf ,
} ,
/// Export memories in JSON or JSONL format
Export {
/// Output file path
output : PathBuf ,
/// Export format: json or jsonl
#[ arg(long, default_value = " json " ) ]
format : String ,
/// Filter by tags (comma-separated)
#[ arg(long) ]
tags : Option < String > ,
/// Only export memories created after this date (YYYY-MM-DD)
#[ arg(long) ]
since : Option < String > ,
} ,
/// Garbage collect stale memories below retention threshold
Gc {
/// Minimum retention strength to keep (delete below this)
#[ arg(long, default_value = " 0.1 " ) ]
min_retention : f64 ,
/// Maximum age in days (delete memories older than this AND below retention threshold)
#[ arg(long) ]
max_age_days : Option < u64 > ,
/// Dry run - show what would be deleted without actually deleting
#[ arg(long) ]
dry_run : bool ,
/// Skip confirmation prompt
#[ arg(long) ]
yes : bool ,
} ,
feat: Vestige v1.2.0 — dashboard, temporal tools, maintenance tools, detail levels
Add web dashboard (axum) on port 3927 with memory browser, search, and
system stats. New MCP tools: memory_timeline, memory_changelog,
health_check, consolidate, stats, backup, export, gc. Search now supports
detail_level (brief/summary/full) to control token usage. Add backup_to()
and get_recent_state_transitions() to storage layer. Bump to v1.2.0.
2026-02-12 04:33:05 -06:00
/// Launch the memory web dashboard
Dashboard {
/// Port to bind the dashboard server to
#[ arg(long, default_value = " 3927 " ) ]
port : u16 ,
/// Don't automatically open the browser
#[ arg(long) ]
no_open : bool ,
} ,
2026-02-12 05:02:09 -06:00
/// Ingest a memory (routes through Prediction Error Gating)
Ingest {
/// Content to remember
content : String ,
/// Tags (comma-separated)
#[ arg(long) ]
tags : Option < String > ,
/// Node type (fact, concept, event, person, place, note, pattern, decision)
#[ arg(long, default_value = " fact " ) ]
node_type : String ,
/// Source reference
#[ arg(long) ]
source : Option < String > ,
} ,
2026-01-26 01:31:58 -06:00
}
fn main ( ) -> anyhow ::Result < ( ) > {
let cli = Cli ::parse ( ) ;
match cli . command {
Commands ::Stats { tagging , states } = > run_stats ( tagging , states ) ,
Commands ::Health = > run_health ( ) ,
Commands ::Consolidate = > run_consolidate ( ) ,
Commands ::Restore { file } = > run_restore ( file ) ,
2026-02-12 03:02:32 -06:00
Commands ::Backup { output } = > run_backup ( output ) ,
Commands ::Export {
output ,
format ,
tags ,
since ,
} = > run_export ( output , format , tags , since ) ,
Commands ::Gc {
min_retention ,
max_age_days ,
dry_run ,
yes ,
} = > run_gc ( min_retention , max_age_days , dry_run , yes ) ,
feat: Vestige v1.2.0 — dashboard, temporal tools, maintenance tools, detail levels
Add web dashboard (axum) on port 3927 with memory browser, search, and
system stats. New MCP tools: memory_timeline, memory_changelog,
health_check, consolidate, stats, backup, export, gc. Search now supports
detail_level (brief/summary/full) to control token usage. Add backup_to()
and get_recent_state_transitions() to storage layer. Bump to v1.2.0.
2026-02-12 04:33:05 -06:00
Commands ::Dashboard { port , no_open } = > run_dashboard ( port , ! no_open ) ,
2026-02-12 05:02:09 -06:00
Commands ::Ingest {
content ,
tags ,
node_type ,
source ,
} = > run_ingest ( content , tags , node_type , source ) ,
2026-01-26 01:31:58 -06:00
}
}
/// Run stats command
fn run_stats ( show_tagging : bool , show_states : bool ) -> anyhow ::Result < ( ) > {
let storage = Storage ::new ( None ) ? ;
let stats = storage . get_stats ( ) ? ;
println! ( " {} " , " === Vestige Memory Statistics === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
// Basic stats
println! ( " {} : {} " , " Total Memories " . white ( ) . bold ( ) , stats . total_nodes ) ;
println! ( " {} : {} " , " Due for Review " . white ( ) . bold ( ) , stats . nodes_due_for_review ) ;
println! ( " {} : {:.1} % " , " Average Retention " . white ( ) . bold ( ) , stats . average_retention * 100.0 ) ;
println! ( " {} : {:.2} " , " Average Storage Strength " . white ( ) . bold ( ) , stats . average_storage_strength ) ;
println! ( " {} : {:.2} " , " Average Retrieval Strength " . white ( ) . bold ( ) , stats . average_retrieval_strength ) ;
println! ( " {} : {} " , " With Embeddings " . white ( ) . bold ( ) , stats . nodes_with_embeddings ) ;
if let Some ( model ) = & stats . embedding_model {
println! ( " {} : {} " , " Embedding Model " . white ( ) . bold ( ) , model ) ;
}
if let Some ( oldest ) = stats . oldest_memory {
println! ( " {} : {} " , " Oldest Memory " . white ( ) . bold ( ) , oldest . format ( " %Y-%m-%d %H:%M:%S " ) ) ;
}
if let Some ( newest ) = stats . newest_memory {
println! ( " {} : {} " , " Newest Memory " . white ( ) . bold ( ) , newest . format ( " %Y-%m-%d %H:%M:%S " ) ) ;
}
// Embedding coverage
let embedding_coverage = if stats . total_nodes > 0 {
( stats . nodes_with_embeddings as f64 / stats . total_nodes as f64 ) * 100.0
} else {
0.0
} ;
println! ( " {} : {:.1} % " , " Embedding Coverage " . white ( ) . bold ( ) , embedding_coverage ) ;
// Tagging distribution (retention levels)
if show_tagging {
println! ( ) ;
println! ( " {} " , " === Retention Distribution === " . yellow ( ) . bold ( ) ) ;
let memories = storage . get_all_nodes ( 500 , 0 ) ? ;
let total = memories . len ( ) ;
if total > 0 {
let high = memories . iter ( ) . filter ( | m | m . retention_strength > = 0.7 ) . count ( ) ;
let medium = memories . iter ( ) . filter ( | m | m . retention_strength > = 0.4 & & m . retention_strength < 0.7 ) . count ( ) ;
let low = memories . iter ( ) . filter ( | m | m . retention_strength < 0.4 ) . count ( ) ;
print_distribution_bar ( " High (>=70%) " , high , total , " green " ) ;
print_distribution_bar ( " Medium (40-70%) " , medium , total , " yellow " ) ;
print_distribution_bar ( " Low (<40%) " , low , total , " red " ) ;
} else {
println! ( " {} " , " No memories found. " . dimmed ( ) ) ;
}
}
// State distribution
if show_states {
println! ( ) ;
println! ( " {} " , " === Cognitive State Distribution === " . magenta ( ) . bold ( ) ) ;
let memories = storage . get_all_nodes ( 500 , 0 ) ? ;
let total = memories . len ( ) ;
if total > 0 {
let ( active , dormant , silent , unavailable ) = compute_state_distribution ( & memories ) ;
print_distribution_bar ( " Active " , active , total , " green " ) ;
print_distribution_bar ( " Dormant " , dormant , total , " yellow " ) ;
print_distribution_bar ( " Silent " , silent , total , " red " ) ;
print_distribution_bar ( " Unavailable " , unavailable , total , " magenta " ) ;
println! ( ) ;
println! ( " {} " , " State Thresholds: " . dimmed ( ) ) ;
println! ( " {} >= 0.70 accessibility " , " Active " . green ( ) ) ;
println! ( " {} >= 0.40 accessibility " , " Dormant " . yellow ( ) ) ;
println! ( " {} >= 0.10 accessibility " , " Silent " . red ( ) ) ;
println! ( " {} < 0.10 accessibility " , " Unavailable " . magenta ( ) ) ;
} else {
println! ( " {} " , " No memories found. " . dimmed ( ) ) ;
}
}
Ok ( ( ) )
}
/// Compute cognitive state distribution for memories
fn compute_state_distribution ( memories : & [ vestige_core ::KnowledgeNode ] ) -> ( usize , usize , usize , usize ) {
let mut active = 0 ;
let mut dormant = 0 ;
let mut silent = 0 ;
let mut unavailable = 0 ;
for memory in memories {
// Accessibility = 0.5*retention + 0.3*retrieval + 0.2*storage
let accessibility = memory . retention_strength * 0.5
+ memory . retrieval_strength * 0.3
+ memory . storage_strength * 0.2 ;
if accessibility > = 0.7 {
active + = 1 ;
} else if accessibility > = 0.4 {
dormant + = 1 ;
} else if accessibility > = 0.1 {
silent + = 1 ;
} else {
unavailable + = 1 ;
}
}
( active , dormant , silent , unavailable )
}
/// Print a distribution bar
fn print_distribution_bar ( label : & str , count : usize , total : usize , color : & str ) {
let percentage = if total > 0 {
( count as f64 / total as f64 ) * 100.0
} else {
0.0
} ;
let bar_width : usize = 30 ;
let filled = ( ( percentage / 100.0 ) * bar_width as f64 ) as usize ;
let empty = bar_width . saturating_sub ( filled ) ;
let bar = format! ( " {} {} " , " # " . repeat ( filled ) , " - " . repeat ( empty ) ) ;
let colored_bar = match color {
" green " = > bar . green ( ) ,
" yellow " = > bar . yellow ( ) ,
" red " = > bar . red ( ) ,
" magenta " = > bar . magenta ( ) ,
_ = > bar . white ( ) ,
} ;
println! (
" {:15} [{:30}] {:>4} ({:>5.1}%) " ,
label ,
colored_bar ,
count ,
percentage
) ;
}
/// Run health check
fn run_health ( ) -> anyhow ::Result < ( ) > {
let storage = Storage ::new ( None ) ? ;
let stats = storage . get_stats ( ) ? ;
println! ( " {} " , " === Vestige Health Check === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
// Determine health status
let ( status , status_color ) = if stats . total_nodes = = 0 {
( " EMPTY " , " white " )
} else if stats . average_retention < 0.3 {
( " CRITICAL " , " red " )
} else if stats . average_retention < 0.5 {
( " DEGRADED " , " yellow " )
} else {
( " HEALTHY " , " green " )
} ;
let colored_status = match status_color {
" green " = > status . green ( ) . bold ( ) ,
" yellow " = > status . yellow ( ) . bold ( ) ,
" red " = > status . red ( ) . bold ( ) ,
_ = > status . white ( ) . bold ( ) ,
} ;
println! ( " {} : {} " , " Status " . white ( ) . bold ( ) , colored_status ) ;
println! ( " {} : {} " , " Total Memories " . white ( ) , stats . total_nodes ) ;
println! ( " {} : {} " , " Due for Review " . white ( ) , stats . nodes_due_for_review ) ;
println! ( " {} : {:.1} % " , " Average Retention " . white ( ) , stats . average_retention * 100.0 ) ;
// Embedding coverage
let embedding_coverage = if stats . total_nodes > 0 {
( stats . nodes_with_embeddings as f64 / stats . total_nodes as f64 ) * 100.0
} else {
0.0
} ;
println! ( " {} : {:.1} % " , " Embedding Coverage " . white ( ) , embedding_coverage ) ;
println! ( " {} : {} " , " Embedding Service " . white ( ) ,
if storage . is_embedding_ready ( ) { " Ready " . green ( ) } else { " Not Ready " . red ( ) } ) ;
// Warnings
let mut warnings = Vec ::new ( ) ;
if stats . average_retention < 0.5 & & stats . total_nodes > 0 {
warnings . push ( " Low average retention - consider running consolidation or reviewing memories " ) ;
}
if stats . nodes_due_for_review > 10 {
warnings . push ( " Many memories are due for review " ) ;
}
if stats . total_nodes > 0 & & stats . nodes_with_embeddings = = 0 {
warnings . push ( " No embeddings generated - semantic search unavailable " ) ;
}
if embedding_coverage < 50.0 & & stats . total_nodes > 10 {
warnings . push ( " Low embedding coverage - run consolidation to improve semantic search " ) ;
}
if ! warnings . is_empty ( ) {
println! ( ) ;
println! ( " {} " , " Warnings: " . yellow ( ) . bold ( ) ) ;
for warning in & warnings {
println! ( " {} {} " , " ! " . yellow ( ) . bold ( ) , warning . yellow ( ) ) ;
}
}
// Recommendations
let mut recommendations = Vec ::new ( ) ;
if status = = " CRITICAL " {
recommendations . push ( " CRITICAL: Many memories have very low retention. Review important memories. " ) ;
}
if stats . nodes_due_for_review > 5 {
recommendations . push ( " Review due memories to strengthen retention. " ) ;
}
if stats . nodes_with_embeddings < stats . total_nodes {
recommendations . push ( " Run 'vestige consolidate' to generate embeddings for better semantic search. " ) ;
}
if stats . total_nodes > 100 & & stats . average_retention < 0.7 {
recommendations . push ( " Consider running periodic consolidation to maintain memory health. " ) ;
}
if recommendations . is_empty ( ) & & status = = " HEALTHY " {
recommendations . push ( " Memory system is healthy! " ) ;
}
println! ( ) ;
println! ( " {} " , " Recommendations: " . cyan ( ) . bold ( ) ) ;
for rec in & recommendations {
let icon = if rec . starts_with ( " CRITICAL " ) { " ! " . red ( ) . bold ( ) } else { " > " . cyan ( ) } ;
let text = if rec . starts_with ( " CRITICAL " ) { rec . red ( ) . to_string ( ) } else { rec . to_string ( ) } ;
println! ( " {} {} " , icon , text ) ;
}
Ok ( ( ) )
}
/// Run consolidation cycle
fn run_consolidate ( ) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Consolidation === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
println! ( " Running memory consolidation cycle... " ) ;
println! ( ) ;
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = Storage ::new ( None ) ? ;
2026-01-26 01:31:58 -06:00
let result = storage . run_consolidation ( ) ? ;
println! ( " {} : {} " , " Nodes Processed " . white ( ) . bold ( ) , result . nodes_processed ) ;
println! ( " {} : {} " , " Nodes Promoted " . white ( ) . bold ( ) , result . nodes_promoted ) ;
println! ( " {} : {} " , " Nodes Pruned " . white ( ) . bold ( ) , result . nodes_pruned ) ;
println! ( " {} : {} " , " Decay Applied " . white ( ) . bold ( ) , result . decay_applied ) ;
println! ( " {} : {} " , " Embeddings Generated " . white ( ) . bold ( ) , result . embeddings_generated ) ;
println! ( " {} : {} ms " , " Duration " . white ( ) . bold ( ) , result . duration_ms ) ;
println! ( ) ;
println! (
" {} " ,
format! (
" Consolidation complete: {} nodes processed, {} embeddings generated in {}ms " ,
result . nodes_processed , result . embeddings_generated , result . duration_ms
)
. green ( )
) ;
Ok ( ( ) )
}
/// Run restore from backup
fn run_restore ( backup_path : PathBuf ) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Restore === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
println! ( " Loading backup from: {} " , backup_path . display ( ) ) ;
// Read and parse backup
let backup_content = std ::fs ::read_to_string ( & backup_path ) ? ;
#[ derive(serde::Deserialize) ]
struct BackupWrapper {
#[ serde(rename = " type " ) ]
_type : String ,
text : String ,
}
#[ derive(serde::Deserialize) ]
struct RecallResult {
results : Vec < MemoryBackup > ,
}
#[ derive(serde::Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct MemoryBackup {
content : String ,
node_type : Option < String > ,
tags : Option < Vec < String > > ,
source : Option < String > ,
}
let wrapper : Vec < BackupWrapper > = serde_json ::from_str ( & backup_content ) ? ;
let recall_result : RecallResult = serde_json ::from_str ( & wrapper [ 0 ] . text ) ? ;
let memories = recall_result . results ;
println! ( " Found {} memories to restore " , memories . len ( ) ) ;
println! ( ) ;
// Initialize storage
println! ( " Initializing storage... " ) ;
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = Storage ::new ( None ) ? ;
2026-01-26 01:31:58 -06:00
println! ( " Generating embeddings and ingesting memories... " ) ;
println! ( ) ;
let total = memories . len ( ) ;
let mut success_count = 0 ;
for ( i , memory ) in memories . into_iter ( ) . enumerate ( ) {
let input = IngestInput {
content : memory . content . clone ( ) ,
node_type : memory . node_type . unwrap_or_else ( | | " fact " . to_string ( ) ) ,
source : memory . source ,
sentiment_score : 0.0 ,
sentiment_magnitude : 0.0 ,
tags : memory . tags . unwrap_or_default ( ) ,
valid_from : None ,
valid_until : None ,
} ;
match storage . ingest ( input ) {
Ok ( _node ) = > {
success_count + = 1 ;
println! (
" [{}/{}] {} {} " ,
i + 1 ,
total ,
" OK " . green ( ) ,
truncate ( & memory . content , 60 )
) ;
}
Err ( e ) = > {
println! ( " [ {} / {} ] {} {} " , i + 1 , total , " FAIL " . red ( ) , e ) ;
}
}
}
println! ( ) ;
println! (
" Restore complete: {}/{} memories restored " ,
success_count . to_string ( ) . green ( ) . bold ( ) ,
total
) ;
// Show stats
let stats = storage . get_stats ( ) ? ;
println! ( ) ;
println! ( " {} : {} " , " Total Nodes " . white ( ) , stats . total_nodes ) ;
println! ( " {} : {} " , " With Embeddings " . white ( ) , stats . nodes_with_embeddings ) ;
Ok ( ( ) )
}
2026-02-12 03:02:32 -06:00
/// Get the default database path
fn get_default_db_path ( ) -> anyhow ::Result < PathBuf > {
let proj_dirs = ProjectDirs ::from ( " com " , " vestige " , " core " )
. ok_or_else ( | | anyhow ::anyhow! ( " Could not determine project directories " ) ) ? ;
Ok ( proj_dirs . data_dir ( ) . join ( " vestige.db " ) )
}
/// Fetch all nodes from storage using pagination
fn fetch_all_nodes ( storage : & Storage ) -> anyhow ::Result < Vec < vestige_core ::KnowledgeNode > > {
let mut all_nodes = Vec ::new ( ) ;
let page_size = 500 ;
let mut offset = 0 ;
loop {
let batch = storage . get_all_nodes ( page_size , offset ) ? ;
let batch_len = batch . len ( ) ;
all_nodes . extend ( batch ) ;
if batch_len < page_size as usize {
break ;
}
offset + = page_size ;
}
Ok ( all_nodes )
}
/// Run backup command - copies the SQLite database file
fn run_backup ( output : PathBuf ) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Backup === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
let db_path = get_default_db_path ( ) ? ;
if ! db_path . exists ( ) {
anyhow ::bail! ( " Database not found at: {} " , db_path . display ( ) ) ;
}
// Open storage to flush WAL before copying
println! ( " Flushing WAL checkpoint... " ) ;
{
let storage = Storage ::new ( None ) ? ;
// get_stats triggers a read so the connection is active, then drop flushes
let _ = storage . get_stats ( ) ? ;
}
// Also flush WAL directly via a separate connection for safety
{
let conn = rusqlite ::Connection ::open ( & db_path ) ? ;
conn . execute_batch ( " PRAGMA wal_checkpoint(TRUNCATE); " ) ? ;
}
// Create parent directories if needed
chore: license AGPL-3.0, zero clippy warnings, CHANGELOG through v1.6.0
License:
- Replace MIT/Apache-2.0 with AGPL-3.0-only across all crates and npm packages
- Replace LICENSE file with official GNU AGPL-3.0 text
- Remove LICENSE-MIT and LICENSE-APACHE
Code quality:
- Fix all 44 clippy warnings (zero remaining)
- Collapsible if statements, redundant closures, manual Option::map
- Remove duplicate #[allow(dead_code)] attributes in deprecated tool modules
- Add Default impl for CognitiveEngine
- Replace manual sort_by with sort_by_key
Documentation:
- Update CHANGELOG with v1.2.0, v1.3.0, v1.5.0, v1.6.0 entries
- Update README with v1.6.0 highlights and accurate stats (52K lines, 1100+ tests)
- Add fastembed-rs/ to .gitignore
- Add fastembed-rs to workspace exclude
1115 tests passing, zero warnings, RUSTFLAGS="-Dwarnings" clean.
2026-02-19 03:00:39 -06:00
if let Some ( parent ) = output . parent ( )
& & ! parent . exists ( ) {
2026-02-12 03:02:32 -06:00
std ::fs ::create_dir_all ( parent ) ? ;
}
// Copy the database file
println! ( " Copying database... " ) ;
println! ( " {} {} " , " From: " . dimmed ( ) , db_path . display ( ) ) ;
println! ( " {} {} " , " To: " . dimmed ( ) , output . display ( ) ) ;
std ::fs ::copy ( & db_path , & output ) ? ;
let file_size = std ::fs ::metadata ( & output ) ? . len ( ) ;
let size_display = if file_size > = 1024 * 1024 {
format! ( " {:.2} MB " , file_size as f64 / ( 1024.0 * 1024.0 ) )
} else if file_size > = 1024 {
format! ( " {:.1} KB " , file_size as f64 / 1024.0 )
} else {
format! ( " {} bytes " , file_size )
} ;
println! ( ) ;
println! (
" {} " ,
format! ( " Backup complete: {} ( {} ) " , output . display ( ) , size_display )
. green ( )
. bold ( )
) ;
Ok ( ( ) )
}
/// Run export command - exports memories in JSON or JSONL format
fn run_export (
output : PathBuf ,
format : String ,
tags : Option < String > ,
since : Option < String > ,
) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Export === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
// Validate format
if format ! = " json " & & format ! = " jsonl " {
anyhow ::bail! ( " Invalid format '{}'. Must be 'json' or 'jsonl'. " , format ) ;
}
// Parse since date if provided
let since_date = match & since {
Some ( date_str ) = > {
let naive = NaiveDate ::parse_from_str ( date_str , " %Y-%m-%d " )
. map_err ( | e | anyhow ::anyhow! ( " Invalid date '{}': {}. Use YYYY-MM-DD format. " , date_str , e ) ) ? ;
Some (
naive
. and_hms_opt ( 0 , 0 , 0 )
. expect ( " midnight is always valid " )
. and_utc ( ) ,
)
}
None = > None ,
} ;
// Parse tags filter
let tag_filter : Vec < String > = tags
. as_deref ( )
. map ( | t | t . split ( ',' ) . map ( | s | s . trim ( ) . to_string ( ) ) . filter ( | s | ! s . is_empty ( ) ) . collect ( ) )
. unwrap_or_default ( ) ;
let storage = Storage ::new ( None ) ? ;
let all_nodes = fetch_all_nodes ( & storage ) ? ;
// Apply filters
let filtered : Vec < & vestige_core ::KnowledgeNode > = all_nodes
. iter ( )
. filter ( | node | {
// Date filter
chore: license AGPL-3.0, zero clippy warnings, CHANGELOG through v1.6.0
License:
- Replace MIT/Apache-2.0 with AGPL-3.0-only across all crates and npm packages
- Replace LICENSE file with official GNU AGPL-3.0 text
- Remove LICENSE-MIT and LICENSE-APACHE
Code quality:
- Fix all 44 clippy warnings (zero remaining)
- Collapsible if statements, redundant closures, manual Option::map
- Remove duplicate #[allow(dead_code)] attributes in deprecated tool modules
- Add Default impl for CognitiveEngine
- Replace manual sort_by with sort_by_key
Documentation:
- Update CHANGELOG with v1.2.0, v1.3.0, v1.5.0, v1.6.0 entries
- Update README with v1.6.0 highlights and accurate stats (52K lines, 1100+ tests)
- Add fastembed-rs/ to .gitignore
- Add fastembed-rs to workspace exclude
1115 tests passing, zero warnings, RUSTFLAGS="-Dwarnings" clean.
2026-02-19 03:00:39 -06:00
if let Some ( ref since_dt ) = since_date
& & node . created_at < * since_dt {
2026-02-12 03:02:32 -06:00
return false ;
}
// Tag filter: node must contain ALL specified tags
if ! tag_filter . is_empty ( ) {
for tag in & tag_filter {
if ! node . tags . iter ( ) . any ( | t | t = = tag ) {
return false ;
}
}
}
true
} )
. collect ( ) ;
println! ( " {} : {} " , " Format " . white ( ) . bold ( ) , format ) ;
if ! tag_filter . is_empty ( ) {
println! ( " {} : {} " , " Tag filter " . white ( ) . bold ( ) , tag_filter . join ( " , " ) ) ;
}
if let Some ( ref date_str ) = since {
println! ( " {} : {} " , " Since " . white ( ) . bold ( ) , date_str ) ;
}
println! (
" {}: {} / {} total " ,
" Matching " . white ( ) . bold ( ) ,
filtered . len ( ) ,
all_nodes . len ( )
) ;
println! ( ) ;
// Create parent directories if needed
chore: license AGPL-3.0, zero clippy warnings, CHANGELOG through v1.6.0
License:
- Replace MIT/Apache-2.0 with AGPL-3.0-only across all crates and npm packages
- Replace LICENSE file with official GNU AGPL-3.0 text
- Remove LICENSE-MIT and LICENSE-APACHE
Code quality:
- Fix all 44 clippy warnings (zero remaining)
- Collapsible if statements, redundant closures, manual Option::map
- Remove duplicate #[allow(dead_code)] attributes in deprecated tool modules
- Add Default impl for CognitiveEngine
- Replace manual sort_by with sort_by_key
Documentation:
- Update CHANGELOG with v1.2.0, v1.3.0, v1.5.0, v1.6.0 entries
- Update README with v1.6.0 highlights and accurate stats (52K lines, 1100+ tests)
- Add fastembed-rs/ to .gitignore
- Add fastembed-rs to workspace exclude
1115 tests passing, zero warnings, RUSTFLAGS="-Dwarnings" clean.
2026-02-19 03:00:39 -06:00
if let Some ( parent ) = output . parent ( )
& & ! parent . exists ( ) {
2026-02-12 03:02:32 -06:00
std ::fs ::create_dir_all ( parent ) ? ;
}
let file = std ::fs ::File ::create ( & output ) ? ;
let mut writer = BufWriter ::new ( file ) ;
match format . as_str ( ) {
" json " = > {
serde_json ::to_writer_pretty ( & mut writer , & filtered ) ? ;
writer . write_all ( b " \n " ) ? ;
}
" jsonl " = > {
for node in & filtered {
serde_json ::to_writer ( & mut writer , node ) ? ;
writer . write_all ( b " \n " ) ? ;
}
}
_ = > unreachable! ( ) ,
}
writer . flush ( ) ? ;
let file_size = std ::fs ::metadata ( & output ) ? . len ( ) ;
let size_display = if file_size > = 1024 * 1024 {
format! ( " {:.2} MB " , file_size as f64 / ( 1024.0 * 1024.0 ) )
} else if file_size > = 1024 {
format! ( " {:.1} KB " , file_size as f64 / 1024.0 )
} else {
format! ( " {} bytes " , file_size )
} ;
println! (
" {} " ,
format! (
" Exported {} memories to {} ({}, {}) " ,
filtered . len ( ) ,
output . display ( ) ,
format ,
size_display
)
. green ( )
. bold ( )
) ;
Ok ( ( ) )
}
/// Run garbage collection command
fn run_gc (
min_retention : f64 ,
max_age_days : Option < u64 > ,
dry_run : bool ,
yes : bool ,
) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Garbage Collection === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = Storage ::new ( None ) ? ;
2026-02-12 03:02:32 -06:00
let all_nodes = fetch_all_nodes ( & storage ) ? ;
let now = Utc ::now ( ) ;
// Find candidates for deletion
let candidates : Vec < & vestige_core ::KnowledgeNode > = all_nodes
. iter ( )
. filter ( | node | {
// Must be below retention threshold
if node . retention_strength > = min_retention {
return false ;
}
// If max_age_days specified, must also be older than that
if let Some ( max_days ) = max_age_days {
let age_days = ( now - node . created_at ) . num_days ( ) ;
if age_days < 0 | | ( age_days as u64 ) < max_days {
return false ;
}
}
true
} )
. collect ( ) ;
println! ( " {} : {} " , " Min retention threshold " . white ( ) . bold ( ) , min_retention ) ;
if let Some ( max_days ) = max_age_days {
println! ( " {} : {} days " , " Max age " . white ( ) . bold ( ) , max_days ) ;
}
println! (
" {}: {} / {} total " ,
" Candidates for deletion " . white ( ) . bold ( ) ,
candidates . len ( ) ,
all_nodes . len ( )
) ;
if candidates . is_empty ( ) {
println! ( ) ;
println! ( " {} " , " No memories match the garbage collection criteria. " . green ( ) ) ;
return Ok ( ( ) ) ;
}
// Show sample of what would be deleted
println! ( ) ;
println! ( " {} " , " Sample of memories to be removed: " . yellow ( ) . bold ( ) ) ;
let sample_count = candidates . len ( ) . min ( 10 ) ;
for node in candidates . iter ( ) . take ( sample_count ) {
let age_days = ( now - node . created_at ) . num_days ( ) ;
println! (
" {} [ret={:.3}, age={}d] {} " ,
node . id [ .. 8 ] . dimmed ( ) ,
node . retention_strength ,
age_days ,
truncate ( & node . content , 60 ) . dimmed ( )
) ;
}
if candidates . len ( ) > sample_count {
println! (
" {} ... and {} more " ,
" " . dimmed ( ) ,
candidates . len ( ) - sample_count
) ;
}
if dry_run {
println! ( ) ;
println! (
" {} " ,
format! (
" Dry run: {} memories would be deleted. Re-run without --dry-run to delete. " ,
candidates . len ( )
)
. yellow ( )
. bold ( )
) ;
return Ok ( ( ) ) ;
}
// Confirmation prompt (unless --yes)
if ! yes {
println! ( ) ;
print! (
" {} Delete {} memories? This cannot be undone. [y/N] " ,
" WARNING: " . red ( ) . bold ( ) ,
candidates . len ( )
) ;
std ::io ::stdout ( ) . flush ( ) ? ;
let mut input = String ::new ( ) ;
std ::io ::stdin ( ) . read_line ( & mut input ) ? ;
let input = input . trim ( ) . to_lowercase ( ) ;
if input ! = " y " & & input ! = " yes " {
println! ( " {} " , " Aborted. " . yellow ( ) ) ;
return Ok ( ( ) ) ;
}
}
// Perform deletion
let mut deleted = 0 ;
let mut errors = 0 ;
let total_candidates = candidates . len ( ) ;
for node in & candidates {
match storage . delete_node ( & node . id ) {
Ok ( true ) = > deleted + = 1 ,
Ok ( false ) = > errors + = 1 , // node was already gone
Err ( e ) = > {
eprintln! ( " {} Failed to delete {} : {} " , " ERR " . red ( ) , & node . id [ .. 8 ] , e ) ;
errors + = 1 ;
}
}
}
println! ( ) ;
println! (
" {} " ,
format! (
" Garbage collection complete: {}/{} memories deleted{} " ,
deleted ,
total_candidates ,
if errors > 0 {
format! ( " ( {} errors) " , errors )
} else {
String ::new ( )
}
)
. green ( )
. bold ( )
) ;
Ok ( ( ) )
}
2026-02-12 05:02:09 -06:00
/// Ingest a memory via CLI (routes through smart_ingest / PE Gating)
fn run_ingest (
content : String ,
tags : Option < String > ,
node_type : String ,
source : Option < String > ,
) -> anyhow ::Result < ( ) > {
if content . trim ( ) . is_empty ( ) {
anyhow ::bail! ( " Content cannot be empty " ) ;
}
let tag_list : Vec < String > = tags
. as_deref ( )
. map ( | t | {
t . split ( ',' )
. map ( | s | s . trim ( ) . to_string ( ) )
. filter ( | s | ! s . is_empty ( ) )
. collect ( )
} )
. unwrap_or_default ( ) ;
let input = IngestInput {
content : content . clone ( ) ,
node_type ,
source ,
sentiment_score : 0.0 ,
sentiment_magnitude : 0.0 ,
tags : tag_list ,
valid_from : None ,
valid_until : None ,
} ;
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = Storage ::new ( None ) ? ;
2026-02-12 05:02:09 -06:00
// Try smart_ingest (PE Gating) if available, otherwise regular ingest
#[ cfg(all(feature = " embeddings " , feature = " vector-search " )) ]
{
let result = storage . smart_ingest ( input ) ? ;
println! ( " {} " , " === Vestige Ingest === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
println! ( " {} : {} " , " Decision " . white ( ) . bold ( ) , result . decision . green ( ) ) ;
println! ( " {} : {} " , " Node ID " . white ( ) . bold ( ) , result . node . id ) ;
if let Some ( sim ) = result . similarity {
println! ( " {} : {:.3} " , " Similarity " . white ( ) . bold ( ) , sim ) ;
}
if let Some ( pe ) = result . prediction_error {
println! ( " {} : {:.3} " , " Prediction Error " . white ( ) . bold ( ) , pe ) ;
}
println! ( " {} : {} " , " Reason " . white ( ) . bold ( ) , result . reason ) ;
println! ( ) ;
println! (
" {} " ,
format! ( " Memory {} ( {} ) " , result . decision , truncate ( & content , 60 ) )
. green ( )
. bold ( )
) ;
}
#[ cfg(not(all(feature = " embeddings " , feature = " vector-search " ))) ]
{
let node = storage . ingest ( input ) ? ;
println! ( " {} " , " === Vestige Ingest === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
println! ( " {} : create " , " Decision " . white ( ) . bold ( ) ) ;
println! ( " {} : {} " , " Node ID " . white ( ) . bold ( ) , node . id ) ;
println! ( ) ;
println! (
" {} " ,
format! ( " Memory created ( {} ) " , truncate ( & content , 60 ) )
. green ( )
. bold ( )
) ;
}
Ok ( ( ) )
}
feat: Vestige v1.2.0 — dashboard, temporal tools, maintenance tools, detail levels
Add web dashboard (axum) on port 3927 with memory browser, search, and
system stats. New MCP tools: memory_timeline, memory_changelog,
health_check, consolidate, stats, backup, export, gc. Search now supports
detail_level (brief/summary/full) to control token usage. Add backup_to()
and get_recent_state_transitions() to storage layer. Bump to v1.2.0.
2026-02-12 04:33:05 -06:00
/// Run the dashboard web server
fn run_dashboard ( port : u16 , open_browser : bool ) -> anyhow ::Result < ( ) > {
println! ( " {} " , " === Vestige Dashboard === " . cyan ( ) . bold ( ) ) ;
println! ( ) ;
println! ( " Starting dashboard at {} ... " , format! ( " http://127.0.0.1: {} " , port ) . cyan ( ) ) ;
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = Storage ::new ( None ) ? ;
feat: Vestige v1.2.0 — dashboard, temporal tools, maintenance tools, detail levels
Add web dashboard (axum) on port 3927 with memory browser, search, and
system stats. New MCP tools: memory_timeline, memory_changelog,
health_check, consolidate, stats, backup, export, gc. Search now supports
detail_level (brief/summary/full) to control token usage. Add backup_to()
and get_recent_state_transitions() to storage layer. Bump to v1.2.0.
2026-02-12 04:33:05 -06:00
// Try to initialize embeddings for search support
#[ cfg(feature = " embeddings " ) ]
{
if let Err ( e ) = storage . init_embeddings ( ) {
println! (
" {} Embeddings unavailable: {} (search will use keyword-only) " ,
" ! " . yellow ( ) ,
e
) ;
}
}
feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.
New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).
Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.
v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00
let storage = std ::sync ::Arc ::new ( storage ) ;
feat: Vestige v1.2.0 — dashboard, temporal tools, maintenance tools, detail levels
Add web dashboard (axum) on port 3927 with memory browser, search, and
system stats. New MCP tools: memory_timeline, memory_changelog,
health_check, consolidate, stats, backup, export, gc. Search now supports
detail_level (brief/summary/full) to control token usage. Add backup_to()
and get_recent_state_transitions() to storage layer. Bump to v1.2.0.
2026-02-12 04:33:05 -06:00
let rt = tokio ::runtime ::Runtime ::new ( ) ? ;
rt . block_on ( async move {
vestige_mcp ::dashboard ::start_dashboard ( storage , port , open_browser )
. await
. map_err ( | e | anyhow ::anyhow! ( " Dashboard error: {} " , e ) )
} )
}
2026-01-26 01:31:58 -06:00
/// Truncate a string for display (UTF-8 safe)
fn truncate ( s : & str , max_chars : usize ) -> String {
let s = s . replace ( '\n' , " " ) ;
if s . chars ( ) . count ( ) < = max_chars {
s
} else {
let truncated : String = s . chars ( ) . take ( max_chars ) . collect ( ) ;
format! ( " {} ... " , truncated )
}
}