kiromi-ai-cli 0.1.0

Operator and developer CLI for the kiromi-ai-memory store: append, search, snapshot, regenerate, migrate-scheme, gc, audit-tail.
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! `kiromi-ai-memory gc` (Plan 12 phase J).

use std::time::Duration;

use kiromi_ai_memory::GcOpts;

use crate::cli::{GcArgs, GlobalArgs};
use crate::error::{CliError, ExitCode};
use crate::runtime::Runtime;

pub(crate) async fn run(args: GcArgs, globals: &GlobalArgs) -> Result<(), CliError> {
    let rt = Runtime::open(globals).await?;
    let dur = parse_duration(&args.retain_for)?;
    let opts = GcOpts::default()
        .with_retain_for(dur)
        .with_dry_run(args.dry_run);
    let report = rt.mem.gc(opts).await?;
    if rt.json {
        println!(
            "{}",
            serde_json::json!({
                "data_blobs_removed": report.data_blobs_removed,
                "summary_blobs_removed": report.summary_blobs_removed,
                "manifest_blobs_removed": report.manifest_blobs_removed,
                "bytes_freed": report.bytes_freed,
                "dry_run": args.dry_run,
            })
        );
    } else {
        println!(
            "data={} summaries={} manifests={} bytes_freed={} dry_run={}",
            report.data_blobs_removed,
            report.summary_blobs_removed,
            report.manifest_blobs_removed,
            report.bytes_freed,
            args.dry_run,
        );
    }
    rt.mem.close().await?;
    Ok(())
}

fn parse_duration(s: &str) -> Result<Duration, CliError> {
    let s = s.trim();
    if s == "0" {
        return Ok(Duration::ZERO);
    }
    let (num, unit) = match s.find(|c: char| !c.is_ascii_digit()) {
        Some(idx) => (&s[..idx], &s[idx..]),
        None => (s, "s"),
    };
    let n: u64 = num.parse().map_err(|e| CliError {
        kind: ExitCode::Config,
        source: anyhow::anyhow!("retain-for: {e}"),
    })?;
    let mult = match unit {
        "s" => 1,
        "m" => 60,
        "h" => 3600,
        "d" => 86_400,
        other => {
            return Err(CliError {
                kind: ExitCode::Config,
                source: anyhow::anyhow!("unknown retain-for unit: {other}"),
            });
        }
    };
    Ok(Duration::from_secs(n * mult))
}