memrec 0.2.0

Local-first AI memory with project isolation — for terminal, for private use
use clap::Args;
use uuid::Uuid;
use memrec_common::{
    JsonRpcRequest, RequestAction, RequestParams,
    SearchMemoryParams, MemoryType, default_min_score,
};
use crate::client::Client;

#[derive(Args, Debug)]
pub struct SearchArgs {
    #[arg(required = true)]
    pub query: String,
    
    #[arg(short = 'k', long, default_value = "10")]
    pub top_k: usize,
    
    #[arg(long, default_value_t = default_min_score())]
    pub min_score: f32,
    
    #[arg(long)]
    pub project_only: bool,
    
    #[arg(long)]
    pub global_only: bool,
    
    #[arg(long)]
    pub all: bool,
    
    #[arg(long)]
    pub mtype: Option<String>,
    
    #[arg(long)]
    pub human: bool,
}

pub async fn execute(client: &Client, args: SearchArgs, working_dir: Option<String>) -> anyhow::Result<()> {
    let memory_type = args.mtype.and_then(|t| match t.to_lowercase().as_str() {
        "decision" => Some(MemoryType::Decision),
        "knowledge" => Some(MemoryType::Knowledge),
        "context" => Some(MemoryType::Context),
        "preference" => Some(MemoryType::Preference),
        "conversation" => Some(MemoryType::Conversation),
        _ => None,
    });
    
    let project_id = if args.global_only {
        Some(Uuid::nil())
    } else {
        None
    };
    
    let request = JsonRpcRequest::new(
        RequestAction::SearchMemory,
        Some(RequestParams::SearchMemory(SearchMemoryParams {
            query: args.query,
            project_id,
            include_global: !args.project_only,
            project_only: args.project_only,
            global_only: args.global_only,
            cross_project: args.all,
            memory_type,
            top_k: args.top_k,
            min_score: args.min_score,
            working_dir,
        })),
        1,
    );
    
    let response = client.send(&request).await?;
    
    if args.human {
        print_human_output(&response);
    } else {
        println!("{}", serde_json::to_string_pretty(&response)?);
    }
    
    Ok(())
}

fn print_human_output(response: &memrec_common::JsonRpcResponse) {
    if let Some(result) = &response.result {
        match result {
            memrec_common::ResponseResult::SemanticSearchResult(result) => {
                println!("Found {} memories:\n", result.total);
                
                for hit in &result.results {
                    println!("[{}] {} (score: {:.2})",
                        hit.memory_type.to_string().to_uppercase(),
                        truncate(&hit.content_preview, 50),
                        hit.score
                    );
                    println!("  ID: {}", hit.memory_id);
                    if let Some(pid) = hit.project_id {
                        if pid.is_nil() {
                            println!("  Project: (global)");
                        } else {
                            println!("  Project: {}", pid);
                        }
                    }
                    println!("  Tags: {:?}", hit.tags);
                    println!("  Created: {}", hit.created_at.format("%Y-%m-%d"));
                    
                    if hit.is_chunked {
                        println!("  Chunked memory ({}/{}). Use --merge.",
                            hit.chunk_index.unwrap_or(0) + 1,
                            hit.chunk_total.unwrap_or(0)
                        );
                    }
                    println!();
                }
            }
            _ => println!("Unexpected response type")
        }
    }
}

fn truncate(s: &str, max_len: usize) -> String {
    if s.len() > max_len {
        s.chars().take(max_len).collect::<String>() + "..."
    } else {
        s.to_string()
    }
}