// cli/journal.rs — journal subcommand handlers use crate::hippocampus as memory; pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>, dedup: bool) -> Result<(), String> { let path = crate::store::nodes_path(); if !path.exists() { return Err("No node log found".into()); } use std::io::BufReader; let file = std::fs::File::open(&path) .map_err(|e| format!("open {}: {}", path.display(), e))?; let mut reader = BufReader::new(file); // Read all entries, keep last N let mut entries: Vec = Vec::new(); while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) { let log = msg.get_root::() .map_err(|e| format!("read log: {}", e))?; for node_reader in log.get_nodes() .map_err(|e| format!("get nodes: {}", e))? { let node = crate::store::Node::from_capnp_migrate(node_reader)?; entries.push(node); } } // Filter by provenance if specified (substring match) if let Some(prov) = provenance { entries.retain(|n| n.provenance.contains(prov)); } // Dedup: keep only the latest version of each key if dedup { let mut seen = std::collections::HashSet::new(); // Walk backwards so we keep the latest entries = entries.into_iter().rev() .filter(|n| seen.insert(n.key.clone())) .collect(); entries.reverse(); } let start = entries.len().saturating_sub(n); for node in &entries[start..] { let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 { crate::store::format_datetime(node.timestamp) } else { format!("(raw:{})", node.timestamp) }; let del = if node.deleted { " [DELETED]" } else { "" }; if full { println!("--- {} (v{}) {} via {} w={:.3}{} ---", node.key, node.version, ts, node.provenance, node.weight, del); println!("{}\n", node.content); } else { let preview = crate::util::first_n_chars(&node.content, 100).replace('\n', "\\n"); println!(" {} v{} w={:.2}{}", ts, node.version, node.weight, del); println!(" {} via {}", node.key, node.provenance); if !preview.is_empty() { println!(" {}", preview); } println!(); } } Ok(()) } pub async fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<(), String> { let entries = memory::journal_tail(None, Some(n as u64), Some(level as u64), None).await .map_err(|e| e.to_string())?; for entry in entries { if full { println!("--- {} ---", entry.key); println!("{}\n", entry.content); } else { let first_line = entry.content.lines().next().unwrap_or("(empty)"); println!("{}: {}", entry.key, first_line); } } Ok(()) } pub async fn cmd_journal_write(name: &str, text: &[String]) -> Result<(), String> { if text.is_empty() { return Err("journal write requires text".into()); } super::check_dry_run(); let body = text.join(" "); let result = memory::journal_new(None, name, name, &body, Some(0)).await .map_err(|e| e.to_string())?; println!("{}", result); Ok(()) }