diff --git a/hindsight-cli/src/commands/bank.rs b/hindsight-cli/src/commands/bank.rs index 485a44bb..b41e33da 100644 --- a/hindsight-cli/src/commands/bank.rs +++ b/hindsight-cli/src/commands/bank.rs @@ -500,6 +500,8 @@ pub fn delete( pub fn consolidate( client: &ApiClient, bank_id: &str, + wait: bool, + poll_interval: u64, verbose: bool, output_format: OutputFormat, ) -> Result<()> { @@ -517,17 +519,82 @@ pub fn consolidate( match response { Ok(result) => { + let operation_id = result.operation_id.clone(); + if output_format == OutputFormat::Pretty { ui::print_success("Consolidation triggered"); - println!(" {} {}", ui::dim("Operation ID:"), result.operation_id); + println!(" {} {}", ui::dim("Operation ID:"), operation_id); if result.deduplicated { println!(" {} {}", ui::dim("Note:"), "Reusing existing pending consolidation task"); } - println!(); - println!("{}", ui::dim("Use 'hindsight operation get' to check the operation status.")); } else { output::print_output(&result, output_format)?; } + + if !wait { + if output_format == OutputFormat::Pretty { + println!(); + println!("{}", ui::dim("Use --wait to poll for completion, or 'hindsight operation get' to check status.")); + } + return Ok(()); + } + + // Poll for completion + if output_format == OutputFormat::Pretty { + println!(); + println!("{}", ui::dim(&format!("Polling every {}s for completion...", poll_interval))); + } + + let start = std::time::Instant::now(); + loop { + std::thread::sleep(std::time::Duration::from_secs(poll_interval)); + let elapsed = start.elapsed().as_secs(); + + let ops_result = client.list_operations(bank_id, verbose); + match ops_result { + Ok(ops) => { + // Find the operation by ID + let op = ops.operations.iter().find(|o| o.id == operation_id); + + match op.map(|o| o.status.as_str()) { + Some("completed") => { + if output_format == OutputFormat::Pretty { + ui::print_success(&format!("Consolidation completed ({}s)", elapsed)); + } + break; + } + Some("failed") => { + let error_msg = op + .and_then(|o| o.error_message.as_ref()) + .map(|s| s.as_str()) + .unwrap_or("Unknown error"); + if output_format == OutputFormat::Pretty { + ui::print_error(&format!("Consolidation failed: {}", error_msg)); + } + std::process::exit(1); + } + Some(status) => { + if output_format == OutputFormat::Pretty { + println!(" ⏳ {} ({}s elapsed)", status, elapsed); + } + } + None => { + if output_format == OutputFormat::Pretty { + ui::print_warning(&format!("Operation {} not found in list", operation_id)); + } + break; + } + } + } + Err(e) => { + if output_format == OutputFormat::Pretty { + ui::print_error(&format!("Failed to check operation status: {}", e)); + } + return Err(e); + } + } + } + Ok(()) } Err(e) => Err(e), diff --git a/hindsight-cli/src/commands/document.rs b/hindsight-cli/src/commands/document.rs index 863a579f..35a435ec 100644 --- a/hindsight-cli/src/commands/document.rs +++ b/hindsight-cli/src/commands/document.rs @@ -1,4 +1,6 @@ use anyhow::Result; +use chrono::{Duration as ChronoDuration, NaiveDate, Utc}; +use std::collections::BTreeMap; use crate::api::ApiClient; use crate::output::{self, OutputFormat}; use crate::ui; @@ -7,11 +9,17 @@ pub fn list( client: &ApiClient, agent_id: &str, query: Option, + date: Option, limit: i32, offset: i32, verbose: bool, output_format: OutputFormat, ) -> Result<()> { + // If date filter is provided, use the date-aware listing + if date.is_some() { + return list_with_date(client, agent_id, date.as_deref(), verbose, output_format); + } + let spinner = if output_format == OutputFormat::Pretty { Some(ui::create_spinner("Fetching documents...")) } else { @@ -50,6 +58,139 @@ pub fn list( } } +/// List documents with date filtering +fn list_with_date( + client: &ApiClient, + bank_id: &str, + date_filter: Option<&str>, + verbose: bool, + output_format: OutputFormat, +) -> Result<()> { + let spinner = if output_format == OutputFormat::Pretty { + Some(ui::create_spinner("Fetching all documents...")) + } else { + None + }; + + // Fetch all documents with pagination + let all_docs = fetch_all_documents(client, bank_id, verbose)?; + + if let Some(mut sp) = spinner { + sp.finish(); + } + + // Parse the date filter + let target_date = parse_date_filter(date_filter)?; + + // Filter and group documents by date + let mut by_date: BTreeMap> = BTreeMap::new(); + let mut filtered_count = 0; + + for doc in all_docs { + let created_at = doc.get("created_at") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Parse the date part (YYYY-MM-DD) from created_at + let doc_date = created_at.split('T').next().unwrap_or(""); + + // Apply date filter if specified + if let Some(ref target) = target_date { + let target_str = target.format("%Y-%m-%d").to_string(); + if doc_date != target_str { + continue; + } + } + + filtered_count += 1; + by_date.entry(doc_date.to_string()).or_default().push(doc); + } + + // Output + if output_format == OutputFormat::Pretty { + let filter_desc = match date_filter { + None | Some("yesterday") => "yesterday".to_string(), + Some("today") => "today".to_string(), + Some("all") => "all dates".to_string(), + Some(d) => d.to_string(), + }; + + ui::print_info(&format!( + "Documents for bank '{}' (filter: {}, showing: {})", + bank_id, filter_desc, filtered_count + )); + println!(); + + // Show documents grouped by date (reverse order - newest first) + for (date_str, docs) in by_date.iter().rev() { + println!(" {} ({} documents)", date_str, docs.len()); + for doc in docs { + let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("unknown"); + let mem_count = doc.get("memory_unit_count").and_then(|v| v.as_i64()).unwrap_or(0); + println!(" - {} ({} memories)", id, mem_count); + } + println!(); + } + } else { + // JSON/YAML output - convert to a list structure + let output: Vec = by_date.values().flatten().cloned().collect(); + output::print_output(&output, output_format)?; + } + + Ok(()) +} + +/// Fetch all documents with pagination +fn fetch_all_documents( + client: &ApiClient, + bank_id: &str, + verbose: bool, +) -> Result> { + let mut all_docs = Vec::new(); + let mut offset = 0; + let limit = 500; + + loop { + let response = client.list_documents(bank_id, None, Some(limit), Some(offset), verbose)?; + + if response.items.is_empty() { + break; + } + + // Convert Map to Value for each item + for item in response.items { + all_docs.push(serde_json::Value::Object(item)); + } + + offset += limit; + + // Check if we've fetched everything + if all_docs.len() >= response.total as usize { + break; + } + } + + Ok(all_docs) +} + +/// Parse date filter string into a NaiveDate +fn parse_date_filter(filter: Option<&str>) -> Result> { + match filter { + None | Some("yesterday") => { + // Default to yesterday + Ok(Some(Utc::now().date_naive() - ChronoDuration::days(1))) + } + Some("today") => Ok(Some(Utc::now().date_naive())), + Some("all") => Ok(None), // No filtering + Some(date_str) => { + // Try to parse as YYYY-MM-DD + NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + .map(Some) + .map_err(|e| anyhow::anyhow!("Invalid date format '{}': {}. Use YYYY-MM-DD, 'yesterday', 'today', or 'all'", date_str, e)) + } + } +} + pub fn get( client: &ApiClient, agent_id: &str, diff --git a/hindsight-cli/src/main.rs b/hindsight-cli/src/main.rs index b9d0cff9..4f8c32af 100644 --- a/hindsight-cli/src/main.rs +++ b/hindsight-cli/src/main.rs @@ -260,6 +260,14 @@ enum BankCommands { Consolidate { /// Bank ID bank_id: String, + + /// Wait for consolidation to complete (poll for status) + #[arg(long)] + wait: bool, + + /// Poll interval in seconds (only used with --wait) + #[arg(long, default_value = "10")] + poll_interval: u64, }, /// Clear all observations for a bank @@ -441,6 +449,10 @@ enum DocumentCommands { #[arg(short = 'q', long)] query: Option, + /// Filter by date (yesterday, today, YYYY-MM-DD, or all) + #[arg(short = 'd', long)] + date: Option, + /// Maximum number of results #[arg(short = 'l', long, default_value = "100")] limit: i32, @@ -754,8 +766,8 @@ fn run() -> Result<()> { BankCommands::Delete { bank_id, yes } => { commands::bank::delete(&client, &bank_id, yes, verbose, output_format) } - BankCommands::Consolidate { bank_id } => { - commands::bank::consolidate(&client, &bank_id, verbose, output_format) + BankCommands::Consolidate { bank_id, wait, poll_interval } => { + commands::bank::consolidate(&client, &bank_id, wait, poll_interval, verbose, output_format) } BankCommands::ClearObservations { bank_id, yes } => { commands::bank::clear_observations(&client, &bank_id, yes, verbose, output_format) @@ -792,8 +804,8 @@ fn run() -> Result<()> { // Document commands Commands::Document(doc_cmd) => match doc_cmd { - DocumentCommands::List { bank_id, query, limit, offset } => { - commands::document::list(&client, &bank_id, query, limit, offset, verbose, output_format) + DocumentCommands::List { bank_id, query, date, limit, offset } => { + commands::document::list(&client, &bank_id, query, date, limit, offset, verbose, output_format) } DocumentCommands::Get { bank_id, document_id } => { commands::document::get(&client, &bank_id, &document_id, verbose, output_format)