// cli/misc.rs — misc subcommand handlers pub fn cmd_search(terms: &[String], pipeline_args: &[String], expand: bool, full: bool, debug: bool, fuzzy: bool, content: bool) -> Result<(), String> { use std::collections::BTreeMap; // When running inside an agent session, exclude already-surfaced nodes let seen = crate::memory_search::HookSession::from_env() .map(|s| s.seen()) .unwrap_or_default(); // Parse pipeline stages (unified: algorithms, filters, transforms, generators) let stages: Vec = if pipeline_args.is_empty() { vec![crate::search::Stage::Algorithm(crate::search::AlgoStage::parse("spread").unwrap())] } else { pipeline_args.iter() .map(|a| crate::search::Stage::parse(a)) .collect::, _>>()? }; // Check if pipeline needs full Store (has filters/transforms/generators) let needs_store = stages.iter().any(|s| !matches!(s, crate::search::Stage::Algorithm(_))); // Check if pipeline starts with a generator (doesn't need seed terms) let has_generator = stages.first().map(|s| matches!(s, crate::search::Stage::Generator(_))).unwrap_or(false); if terms.is_empty() && !has_generator { return Err("search requires terms or a generator stage (e.g. 'all')".into()); } let query: String = terms.join(" "); if debug { let names: Vec = stages.iter().map(|s| format!("{}", s)).collect(); println!("[search] pipeline: {}", names.join(" → ")); } let max_results = if expand { 15 } else { 5 }; if needs_store { // Full Store path — needed for filter/transform/generator stages let store = crate::store::Store::load()?; let graph = store.build_graph(); let seeds = if has_generator { vec![] // generator will produce its own result set } else { let terms_map: BTreeMap = query.split_whitespace() .map(|t| (t.to_lowercase(), 1.0)) .collect(); let (seeds, _) = crate::search::match_seeds_opts(&terms_map, &store, fuzzy, content); seeds }; let raw = crate::search::run_query(&stages, seeds, &graph, &store, debug, max_results); let raw: Vec<_> = raw.into_iter() .filter(|(key, _)| !seen.contains(key)) .collect(); if raw.is_empty() { eprintln!("No results"); return Ok(()); } for (i, (key, score)) in raw.iter().enumerate().take(max_results) { let weight = store.nodes.get(key).map(|n| n.weight).unwrap_or(0.0); println!("{:2}. [{:.2}/{:.2}] {}", i + 1, score, weight, key); if full && let Some(node) = store.nodes.get(key) { println!(); for line in node.content.lines() { println!(" {}", line); } println!(); } } } else { // Fast MmapView path — algorithm-only pipeline use crate::store::StoreView; let view = crate::store::AnyView::load()?; let graph = crate::graph::build_graph_fast(&view); let terms_map: BTreeMap = query.split_whitespace() .map(|t| (t.to_lowercase(), 1.0)) .collect(); let (seeds, direct_hits) = crate::search::match_seeds_opts(&terms_map, &view, fuzzy, content); if seeds.is_empty() { eprintln!("No results for '{}'", query); return Ok(()); } if debug { println!("[search] {} seeds from query '{}'", seeds.len(), query); } // Extract AlgoStages from the unified stages let algo_stages: Vec<&crate::search::AlgoStage> = stages.iter() .filter_map(|s| match s { crate::search::Stage::Algorithm(a) => Some(a), _ => None, }) .collect(); let algo_owned: Vec = algo_stages.into_iter().cloned().collect(); let raw = crate::search::run_pipeline(&algo_owned, seeds, &graph, &view, debug, max_results); let results: Vec = raw.into_iter() .filter(|(key, _)| !seen.contains(key)) .map(|(key, activation)| { let is_direct = direct_hits.contains(&key); crate::search::SearchResult { key, activation, is_direct, snippet: None } }) .collect(); if results.is_empty() { eprintln!("No results for '{}'", query); return Ok(()); } // Log retrieval crate::store::Store::log_retrieval_static(&query, &results.iter().map(|r| r.key.clone()).collect::>()); let bump_keys: Vec<&str> = results.iter().take(max_results).map(|r| r.key.as_str()).collect(); let _ = crate::lookups::bump_many(&bump_keys); for (i, r) in results.iter().enumerate().take(max_results) { let marker = if r.is_direct { "→" } else { " " }; let weight = view.node_weight(&r.key); println!("{}{:2}. [{:.2}/{:.2}] {}", marker, i + 1, r.activation, weight, r.key); if full && let Some(content) = view.node_content(&r.key) { println!(); for line in content.lines() { println!(" {}", line); } println!(); } } } Ok(()) } pub fn cmd_status() -> Result<(), String> { // If stdout is a tty and daemon is running, launch TUI if std::io::IsTerminal::is_terminal(&std::io::stdout()) { // Try TUI first — falls back if daemon not running match crate::tui::run_tui() { Ok(()) => return Ok(()), Err(_) => {} // fall through to text output } } let store = crate::store::Store::load()?; let g = store.build_graph(); let mut type_counts = std::collections::HashMap::new(); for node in store.nodes.values() { *type_counts.entry(format!("{:?}", node.node_type)).or_insert(0usize) += 1; } let mut types: Vec<_> = type_counts.iter().collect(); types.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); println!("Nodes: {} Relations: {}", store.nodes.len(), store.relations.len()); print!("Types:"); for (t, c) in &types { let label = match t.as_str() { "Semantic" => "semantic", "EpisodicSession" | "EpisodicDaily" | "EpisodicWeekly" | "EpisodicMonthly" => "episodic", _ => t, }; print!(" {}={}", label, c); } println!(); println!("Graph edges: {} Communities: {}", g.edge_count(), g.community_count()); Ok(()) } pub fn cmd_log() -> Result<(), String> { let store = crate::store::Store::load()?; for event in store.retrieval_log.iter().rev().take(20) { println!("[{}] q=\"{}\" → {} results", event.timestamp, event.query, event.results.len()); for r in &event.results { println!(" {}", r); } } Ok(()) } pub fn cmd_params() -> Result<(), String> { let store = crate::store::Store::load()?; println!("decay_factor: {}", store.params.decay_factor); println!("use_boost: {}", store.params.use_boost); println!("prune_threshold: {}", store.params.prune_threshold); println!("edge_decay: {}", store.params.edge_decay); println!("max_hops: {}", store.params.max_hops); println!("min_activation: {}", store.params.min_activation); 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 store = crate::store::Store::load()?; let graph = store.build_graph(); crate::query_parser::run_query(&store, &graph, &query_str) } pub fn get_group_content(group: &crate::config::ContextGroup, store: &crate::store::Store, cfg: &crate::config::Config) -> Vec<(String, String)> { match group.source { crate::config::ContextSource::Journal => { let mut entries = Vec::new(); let now = crate::store::now_epoch(); let window: i64 = cfg.journal_days as i64 * 24 * 3600; let cutoff = now - window; let key_date_re = regex::Regex::new(r"j-(\d{4}-\d{2}-\d{2})").unwrap(); let journal_ts = |n: &crate::store::Node| -> i64 { if n.created_at > 0 { return n.created_at; } if let Some(caps) = key_date_re.captures(&n.key) { use chrono::{NaiveDate, TimeZone, Local}; if let Ok(d) = NaiveDate::parse_from_str(&caps[1], "%Y-%m-%d") && let Some(dt) = Local.from_local_datetime(&d.and_hms_opt(0, 0, 0).unwrap()).earliest() { return dt.timestamp(); } } n.timestamp }; let mut journal_nodes: Vec<_> = store.nodes.values() .filter(|n| n.node_type == crate::store::NodeType::EpisodicSession && journal_ts(n) >= cutoff) .collect(); journal_nodes.sort_by_key(|n| journal_ts(n)); let max = cfg.journal_max; let skip = journal_nodes.len().saturating_sub(max); for node in journal_nodes.iter().skip(skip) { entries.push((node.key.clone(), node.content.clone())); } entries } 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 = store.render_file(key)?; if content.trim().is_empty() { return None; } Some((key.clone(), content.trim().to_string())) }).collect() } } } /// MCP tool schema with CLI routing info. /// /// Each tool definition includes: /// - name, description, inputSchema (standard MCP) /// - cli: the CLI args prefix to invoke this tool /// - stdin_param: which parameter (if any) should be sent via stdin /// /// Tools with cli=null are agent-internal (not exposed via MCP CLI bridge). pub fn cmd_mcp_schema() -> Result<(), String> { use serde_json::json; // Map tool names to CLI args + stdin param. // Tools not listed here are skipped (agent-internal). let cli_map: std::collections::HashMap<&str, (Vec<&str>, Option<&str>)> = [ ("memory_render", (vec!["render"], None)), ("memory_write", (vec!["write"], Some("content"))), ("memory_search", (vec!["graph", "spread"], None)), ("memory_links", (vec!["graph", "link"], None)), ("memory_link_set", (vec!["graph", "link-set"], None)), ("memory_link_add", (vec!["graph", "link-add"], None)), ("memory_used", (vec!["used"], None)), ("memory_weight_set", (vec!["weight-set"], None)), ("memory_rename", (vec!["node", "rename"], None)), ("memory_query", (vec!["query"], None)), ].into_iter().collect(); let defs = crate::thought::memory::definitions(); let json_out: Vec<_> = defs.iter().filter_map(|d| { let name = &d.function.name; let (cli, stdin_param) = cli_map.get(name.as_str())?; Some(json!({ "name": name, "description": d.function.description, "inputSchema": d.function.parameters, "cli": cli, "stdin_param": stdin_param, })) }).collect(); println!("{}", serde_json::to_string_pretty(&json_out) .map_err(|e| e.to_string())?); Ok(()) } pub fn cmd_load_context(stats: bool) -> Result<(), String> { let cfg = crate::config::get(); let store = crate::store::Store::load()?; 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, &store, &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); println!(); for group in &cfg.context_groups { let entries = get_group_content(group, &store, &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(()) }