// cli/node.rs — node subcommand handlers // // render, write, node-delete, node-rename, history, list-keys, // list-edges, dump-json, lookup-bump, lookups. use crate::store; pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> { super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_weight_set", serde_json::json!({"key": key, "weight": weight}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_node_delete(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("node-delete requires a key".into()); } super::check_dry_run(); let key = key.join(" "); let result = crate::mcp_server::memory_rpc( "memory_delete", serde_json::json!({"key": key}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> { super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_rename", serde_json::json!({"old_key": old_key, "new_key": new_key}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_render(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("render requires a key".into()); } let key = key.join(" "); let bare = store::strip_md_suffix(&key); let rendered = crate::mcp_server::memory_rpc( "memory_render", serde_json::json!({"key": bare}), ).map_err(|e| e.to_string())?; 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, bare); } } Ok(()) } pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> { if key.is_empty() { return Err("history requires a key".into()); } let key = key.join(" "); let result = crate::mcp_server::memory_rpc( "memory_history", serde_json::json!({"key": key, "full": full}), ).map_err(|e| e.to_string())?; print!("{}", result); Ok(()) } pub fn cmd_write(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("write requires a key (reads content from stdin)".into()); } let key = key.join(" "); let mut content = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut content) .map_err(|e| format!("read stdin: {}", e))?; if content.trim().is_empty() { return Err("No content on stdin".into()); } super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_write", serde_json::json!({"key": key, "content": content}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_edit(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("edit requires a key".into()); } let key = key.join(" "); // Get raw content via RPC let content = crate::mcp_server::memory_rpc( "memory_render", serde_json::json!({"key": key, "raw": true}), ).unwrap_or_default(); let tmp = std::env::temp_dir().join(format!("poc-memory-edit-{}.md", key.replace('/', "_"))); std::fs::write(&tmp, &content) .map_err(|e| format!("write temp file: {}", e))?; let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into()); let status = std::process::Command::new(&editor) .arg(&tmp) .status() .map_err(|e| format!("spawn {}: {}", editor, e))?; if !status.success() { let _ = std::fs::remove_file(&tmp); return Err(format!("{} exited with {}", editor, status)); } let new_content = std::fs::read_to_string(&tmp) .map_err(|e| format!("read temp file: {}", e))?; let _ = std::fs::remove_file(&tmp); if new_content == content { println!("No change: '{}'", key); return Ok(()); } if new_content.trim().is_empty() { return Err("Content is empty, aborting".into()); } super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_write", serde_json::json!({"key": key, "content": new_content}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_search(keys: &[String]) -> Result<(), String> { if keys.is_empty() { return Err("search requires seed keys".into()); } let result = crate::mcp_server::memory_rpc( "memory_search", serde_json::json!({"keys": keys}), ).map_err(|e| e.to_string())?; print!("{}", result); Ok(()) } pub fn cmd_query(expr: &[String]) -> Result<(), String> { if expr.is_empty() { return Err("query requires an expression (try: poc-memory query --help)".into()); } let query_str = expr.join(" "); let result = crate::mcp_server::memory_rpc( "memory_query", serde_json::json!({"query": query_str}), ).map_err(|e| e.to_string())?; print!("{}", result); Ok(()) } /// Get group content via RPC (handles daemon or local fallback) pub fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::config::Config) -> Vec<(String, String)> { match group.source { crate::config::ContextSource::Journal => { // Query for recent journal entries let window: i64 = cfg.journal_days as i64 * 24 * 3600; let query = format!("all | type:episodic | age:<{} | sort:timestamp | limit:{}", window, cfg.journal_max); let keys_str = match crate::mcp_server::memory_rpc( "memory_query", serde_json::json!({"query": query}), ) { Ok(s) => s, Err(_) => return vec![], }; // Parse keys (one per line) and render each keys_str.lines() .filter(|k| !k.is_empty() && *k != "no results") .filter_map(|key| { let content = crate::mcp_server::memory_rpc( "memory_render", serde_json::json!({"key": key, "raw": true}), ).ok()?; if content.trim().is_empty() { return None; } Some((key.to_string(), content)) }) .collect() } crate::config::ContextSource::File => { group.keys.iter().filter_map(|key| { let content = std::fs::read_to_string(cfg.identity_dir.join(key)).ok()?; if content.trim().is_empty() { return None; } Some((key.clone(), content.trim().to_string())) }).collect() } crate::config::ContextSource::Store => { group.keys.iter().filter_map(|key| { let content = crate::mcp_server::memory_rpc( "memory_render", serde_json::json!({"key": key, "raw": true}), ).ok()?; if content.trim().is_empty() { return None; } Some((key.clone(), content.trim().to_string())) }).collect() } } } pub fn cmd_load_context(stats: bool) -> Result<(), String> { let cfg = crate::config::get(); if stats { let mut total_words = 0; let mut total_entries = 0; println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS"); println!("{}", "-".repeat(42)); for group in &cfg.context_groups { let entries = get_group_content(group, &cfg); let words: usize = entries.iter() .map(|(_, c)| c.split_whitespace().count()) .sum(); let count = entries.len(); println!("{:<25} {:>6} {:>8}", group.label, count, words); total_words += words; total_entries += count; } println!("{}", "-".repeat(42)); println!("{:<25} {:>6} {:>8}", "TOTAL", total_entries, total_words); return Ok(()); } println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name); for group in &cfg.context_groups { let entries = get_group_content(group, &cfg); if !entries.is_empty() && group.source == crate::config::ContextSource::Journal { println!("--- recent journal entries ({}/{}) ---", entries.len(), cfg.journal_max); } for (key, content) in entries { if group.source == crate::config::ContextSource::Journal { println!("## {}", key); } else { println!("--- {} ({}) ---", key, group.label); } println!("{}\n", content); } } println!("=== END MEMORY LOAD ==="); Ok(()) }