// cli/node.rs — node subcommand handlers // // render, write, node-delete, node-rename, history, list-keys, // list-edges, dump-json, lookup-bump, lookups. use anyhow::{bail, Context, Result}; use crate::hippocampus as memory; pub async fn cmd_weight_set(key: &str, weight: f32) -> Result<()> { super::check_dry_run(); let result = memory::memory_weight_set(None, key, weight).await?; println!("{}", result); Ok(()) } pub async fn cmd_node_delete(key: &[String]) -> Result<()> { if key.is_empty() { bail!("node-delete requires a key"); } super::check_dry_run(); let key = key.join(" "); let result = memory::memory_delete(None, &key).await?; println!("{}", result); Ok(()) } pub async fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<()> { super::check_dry_run(); let result = memory::memory_rename(None, old_key, new_key).await?; println!("{}", result); Ok(()) } pub async fn cmd_node_restore(key: &[String]) -> Result<()> { if key.is_empty() { bail!("node-restore requires a key"); } super::check_dry_run(); let key = key.join(" "); let result = memory::memory_restore(None, &key).await?; println!("{}", result); Ok(()) } pub async fn cmd_render(key: &[String]) -> Result<()> { if key.is_empty() { bail!("render requires a key"); } let key = key.join(" "); let rendered = memory::memory_render(None, &key, None).await?; print!("{}", rendered); // Mark as seen if we're inside a Claude session (not an agent subprocess — // agents read the seen set but shouldn't write to it as a side effect of // tool calls; only surface_agent_cycle should mark keys seen) if std::env::var("POC_AGENT").is_err() && let Ok(session_id) = std::env::var("POC_SESSION_ID") && !session_id.is_empty() { let state_dir = crate::store::memory_dir().join("sessions"); let seen_path = state_dir.join(format!("seen-{}", session_id)); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true).append(true).open(seen_path) { use std::io::Write; let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"); let _ = writeln!(f, "{}\t{}", ts, key); } } Ok(()) } pub async fn cmd_history(key: &[String], full: bool) -> Result<()> { if key.is_empty() { bail!("history requires a key"); } let key = key.join(" "); let result = memory::memory_history(None, &key, Some(full)).await?; print!("{}", result); Ok(()) } pub async fn cmd_write(key: &[String]) -> Result<()> { if key.is_empty() { bail!("write requires a key (reads content from stdin)"); } let key = key.join(" "); let mut content = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut content) .context("read stdin")?; if content.trim().is_empty() { bail!("No content on stdin"); } super::check_dry_run(); let result = memory::memory_write(None, &key, &content).await?; println!("{}", result); Ok(()) } pub async fn cmd_edit(key: &[String]) -> Result<()> { if key.is_empty() { bail!("edit requires a key"); } let key = key.join(" "); // Get raw content let content = memory::memory_render(None, &key, Some(true)).await .unwrap_or_default(); let tmp = std::env::temp_dir().join(format!("poc-memory-edit-{}.md", key.replace('/', "_"))); std::fs::write(&tmp, &content) .with_context(|| format!("write temp file {}", tmp.display()))?; let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into()); let status = std::process::Command::new(&editor) .arg(&tmp) .status() .with_context(|| format!("spawn {}", editor))?; if !status.success() { let _ = std::fs::remove_file(&tmp); bail!("{} exited with {}", editor, status); } let new_content = std::fs::read_to_string(&tmp) .with_context(|| format!("read temp file {}", tmp.display()))?; let _ = std::fs::remove_file(&tmp); if new_content == content { println!("No change: '{}'", key); return Ok(()); } if new_content.trim().is_empty() { bail!("Content is empty, aborting"); } super::check_dry_run(); let result = memory::memory_write(None, &key, &new_content).await?; println!("{}", result); Ok(()) } pub async fn cmd_search(keys: &[String]) -> Result<()> { if keys.is_empty() { bail!("search requires seed keys"); } let result = memory::memory_search(None, keys.to_vec(), None, None, None, None).await?; print!("{}", result); Ok(()) } pub async fn cmd_query(expr: &[String]) -> Result<()> { if expr.is_empty() { bail!("query requires an expression (try: poc-memory query --help)"); } let query_str = expr.join(" "); let result = memory::memory_query(None, &query_str, None).await?; print!("{}", result); Ok(()) } /// Load content for a list of node keys. async fn load_nodes(keys: &[String]) -> Vec<(String, String)> { let mut results = Vec::new(); for key in keys { if let Ok(content) = memory::memory_render(None, key, Some(true)).await { if !content.trim().is_empty() { results.push((key.clone(), content.trim().to_string())); } } } results } pub async fn cmd_load_context(stats: bool) -> Result<()> { let cfg = crate::config::get(); let personality = load_nodes(&cfg.personality_nodes).await; let agent = load_nodes(&cfg.agent_nodes).await; if stats { let p_words: usize = personality.iter().map(|(_, c)| c.split_whitespace().count()).sum(); let a_words: usize = agent.iter().map(|(_, c)| c.split_whitespace().count()).sum(); println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS"); println!("{}", "-".repeat(42)); println!("{:<25} {:>6} {:>8}", "personality_nodes", personality.len(), p_words); println!("{:<25} {:>6} {:>8}", "agent_nodes", agent.len(), a_words); println!("{}", "-".repeat(42)); println!("{:<25} {:>6} {:>8}", "TOTAL", personality.len() + agent.len(), p_words + a_words); return Ok(()); } println!("=== MEMORY SYSTEM ({}) ===", crate::config::app().assistant_name); if !personality.is_empty() { println!("--- personality_nodes ({}) ---", personality.len()); for (key, content) in personality { println!("## {}", key); println!("{}\n", content); } } if !agent.is_empty() { println!("--- agent_nodes ({}) ---", agent.len()); for (key, content) in agent { println!("## {}", key); println!("{}\n", content); } } println!("=== END MEMORY LOAD ==="); Ok(()) }