feat(cli): add --wait flag for consolidate and --date filter for document list (#244)

- bank consolidate: add --wait flag to poll for completion status
- bank consolidate: add --poll-interval option (default 10s)
- document list: add --date filter (yesterday, today, YYYY-MM-DD, or all)

[skip ci]

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Anton Evseev 2026-01-30 18:04:35 +10:00 committed by GitHub
parent 751f99a82f
commit ff20bf9dc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 227 additions and 7 deletions

View file

@ -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),

View file

@ -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<String>,
date: Option<String>,
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<String, Vec<serde_json::Value>> = 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<serde_json::Value> = 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<Vec<serde_json::Value>> {
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<String, Value> 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<Option<NaiveDate>> {
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,

View file

@ -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<String>,
/// Filter by date (yesterday, today, YYYY-MM-DD, or all)
#[arg(short = 'd', long)]
date: Option<String>,
/// 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)