// 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; fn stash_path() -> std::path::PathBuf { poc_memory::store::memory_dir().join("sessions/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, /// Session ID (overrides stash file; for multiple concurrent sessions) #[arg(long)] session: Option, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Cmd { /// Run surface agent, parse output, render memories Surface, /// Run reflect agent, dump output Reflect, } fn resolve_session(session_arg: &Option) -> Option { use poc_memory::memory_search::HookSession; if let Some(id) = session_arg { return HookSession::from_id(id.clone()); } let input = fs::read_to_string(stash_path()).ok()?; HookSession::from_json(&input) } fn show_seen(session_arg: &Option) { let Some(session) = resolve_session(session_arg) else { eprintln!("No session state available (use --session ID)"); 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, session_arg: &Option) { let session_id = session_arg.clone() .or_else(|| std::env::var("CLAUDE_SESSION_ID").ok()) .or_else(|| { fs::read_to_string(stash_path()).ok() .and_then(|s| poc_memory::memory_search::HookSession::from_json(&s)) .map(|s| s.session_id) }) .unwrap_or_default(); if session_id.is_empty() { eprintln!("No session ID available (use --session ID, 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); } // Extract the final response — after the last "=== RESPONSE ===" marker let response = result.rsplit_once("=== RESPONSE ===") .map(|(_, rest)| rest.trim()) .unwrap_or(result.trim()); if agent == "reflect" { // Reflect: find REFLECTION marker and dump what follows if let Some(pos) = response.find("REFLECTION") { let after = &response[pos + "REFLECTION".len()..]; let text = after.trim(); if !text.is_empty() { println!("{}", text); } } else if response.contains("NO OUTPUT") { println!("(no reflection)"); } else { eprintln!("Unexpected output format"); println!("{}", response); } return; } // Surface: parse NEW RELEVANT MEMORIES, render them let tail_lines: Vec<&str> = response.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 = response.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!("{}", response); } } fn main() { let args = Args::parse(); if let Some(cmd) = args.command { match cmd { Cmd::Surface => run_agent_and_parse("surface", &args.session), Cmd::Reflect => run_agent_and_parse("reflect", &args.session), } 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(stash_path().parent().unwrap()); let _ = fs::write(stash_path(), &buf); buf } }; let output = poc_memory::memory_search::run_hook(&input); print!("{}", output); } else { show_seen(&args.session) } }