// memory-search CLI — thin wrapper around poc_memory::memory_search // // --hook: run hook logic (for debugging; poc-hook calls the library directly) // surface/reflect: run agent, parse output, render memories to stdout // no args: show seen set for current session use clap::{Parser, Subcommand}; use std::fs; use std::io::{self, Read}; use std::process::Command; const STASH_PATH: &str = "/tmp/claude-memory-search/last-input.json"; #[derive(Parser)] #[command(name = "memory-search")] struct Args { /// Run hook logic (reads JSON from stdin or stash file) #[arg(long)] hook: bool, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Cmd { /// Run surface agent, parse output, render memories Surface, /// Run reflect agent, dump output Reflect, } fn show_seen() { let input = match fs::read_to_string(STASH_PATH) { Ok(s) => s, Err(_) => { eprintln!("No session state available"); return; } }; let Some(session) = poc_memory::memory_search::Session::from_json(&input) else { eprintln!("No session state available"); return; }; println!("Session: {}", session.session_id); if let Ok(cookie) = fs::read_to_string(&session.path("cookie")) { println!("Cookie: {}", cookie.trim()); } match fs::read_to_string(&session.path("compaction")) { Ok(s) => { let offset: u64 = s.trim().parse().unwrap_or(0); let ts = poc_memory::transcript::compaction_timestamp(&session.transcript_path, offset); match ts { Some(t) => println!("Last compaction: offset {} ({})", offset, t), None => println!("Last compaction: offset {}", offset), } } Err(_) => println!("Last compaction: none detected"), } let pending = fs::read_dir(&session.path("chunks")).ok() .map(|d| d.flatten().count()).unwrap_or(0); if pending > 0 { println!("Pending chunks: {}", pending); } for (label, suffix) in [("Current seen set", ""), ("Previous seen set (pre-compaction)", "-prev")] { let path = session.state_dir.join(format!("seen{}-{}", suffix, session.session_id)); let content = fs::read_to_string(&path).unwrap_or_default(); let lines: Vec<&str> = content.lines().filter(|s| !s.is_empty()).collect(); if lines.is_empty() { continue; } println!("\n{} ({}):", label, lines.len()); for line in &lines { println!(" {}", line); } } } fn run_agent_and_parse(agent: &str) { let session_id = std::env::var("CLAUDE_SESSION_ID") .or_else(|_| { fs::read_to_string(STASH_PATH).ok() .and_then(|s| poc_memory::memory_search::Session::from_json(&s)) .map(|s| s.session_id) .ok_or(std::env::VarError::NotPresent) }) .unwrap_or_default(); if session_id.is_empty() { eprintln!("No session ID available (set CLAUDE_SESSION_ID or run --hook first)"); std::process::exit(1); } eprintln!("Running {} agent (session {})...", agent, &session_id[..8.min(session_id.len())]); let output = Command::new("poc-memory") .args(["agent", "run", agent, "--count", "1", "--local"]) .env("POC_SESSION_ID", &session_id) .output(); let output = match output { Ok(o) => o, Err(e) => { eprintln!("Failed to run agent: {}", e); std::process::exit(1); } }; let result = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.is_empty() { eprintln!("{}", stderr); } if agent == "reflect" { // Reflect: just dump the response let lines: Vec<&str> = result.lines().collect(); if let Some(pos) = lines.iter().position(|l| l.starts_with("REFLECTION")) { for line in &lines[pos + 1..] { if !line.trim().is_empty() { println!("{}", line); } } } else if lines.iter().any(|l| l.starts_with("NO OUTPUT")) { println!("(no reflection)"); } else { eprintln!("Unexpected output format"); print!("{}", result); } return; } // Surface: parse NEW RELEVANT MEMORIES, render them let tail_lines: Vec<&str> = result.lines().rev() .filter(|l| !l.trim().is_empty()).take(8).collect(); let has_new = tail_lines.iter().any(|l| l.starts_with("NEW RELEVANT MEMORIES:")); let has_none = tail_lines.iter().any(|l| l.starts_with("NO NEW RELEVANT MEMORIES")); if has_new { let after_marker = result.rsplit_once("NEW RELEVANT MEMORIES:") .map(|(_, rest)| rest).unwrap_or(""); let keys: Vec = after_marker.lines() .map(|l| l.trim().trim_start_matches("- ").trim().to_string()) .filter(|l| !l.is_empty() && !l.starts_with("```")).collect(); if keys.is_empty() { println!("(no memories found)"); return; } let Ok(store) = poc_memory::store::Store::load() else { eprintln!("Failed to load store"); return; }; for key in &keys { if let Some(content) = poc_memory::cli::node::render_node(&store, key) { if !content.trim().is_empty() { println!("--- {} (surfaced) ---", key); print!("{}", content); println!(); } } else { eprintln!(" key not found: {}", key); } } } else if has_none { println!("(no new relevant memories)"); } else { eprintln!("Unexpected output format"); print!("{}", result); } } fn main() { let args = Args::parse(); if let Some(cmd) = args.command { match cmd { Cmd::Surface => run_agent_and_parse("surface"), Cmd::Reflect => run_agent_and_parse("reflect"), } return; } if args.hook { // Read from stdin if piped, otherwise from stash let input = { let mut buf = String::new(); io::stdin().read_to_string(&mut buf).ok(); if buf.trim().is_empty() { fs::read_to_string(STASH_PATH).unwrap_or_default() } else { let _ = fs::create_dir_all("/tmp/claude-memory-search"); let _ = fs::write(STASH_PATH, &buf); buf } }; let output = poc_memory::memory_search::run_hook(&input); print!("{}", output); } else { show_seen() } }