// cli/journal.rs — journal subcommand handlers use anyhow::{bail, Context, Result}; use crate::hippocampus as memory; pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>, dedup: bool) -> Result<()> { let path = crate::store::nodes_path(); if !path.exists() { bail!("No node log found"); } use std::io::BufReader; let file = std::fs::File::open(&path) .with_context(|| format!("open {}", path.display()))?; 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::() .with_context(|| "read log")?; for node_reader in log.get_nodes() .with_context(|| "get nodes")? { 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<()> { let entries = memory::journal_tail(None, Some(n as u64), Some(level as u64), None).await?; 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<()> { if text.is_empty() { bail!("journal write requires text"); } super::check_dry_run(); let body = text.join(" "); let result = memory::journal_new(None, name, name, &body, Some(0)).await?; println!("{}", result); Ok(()) }