agentlog 0.1.2

CLI flight recorder for AI coding agents - capture, store, and replay AI sessions
Documentation
use crate::core::db::DbManager;
use crate::core::{db_path, ensure_initialized};
use anyhow::Result;
use chrono::{Duration, Utc};
use colored::Colorize;

pub async fn execute(
    since: Option<String>,
    until: Option<String>,
    agent: Option<String>,
    file: Option<String>,
) -> Result<()> {
    let project_root = ensure_initialized()?;
    let db_file = db_path(&project_root);

    let db = DbManager::open(&db_file)?;

    let since_dt = since.map(|s| parse_time(&s)).transpose()?;
    let until_dt = until.map(|s| parse_time(&s)).transpose()?;

    let sessions = db.search_sessions(since_dt, until_dt, agent.as_deref(), file.as_deref())?;

    if sessions.is_empty() {
        println!("{}", "No sessions found matching your criteria.".yellow());
        return Ok(());
    }

    println!(
        "{}",
        format!("Found {} session(s)", sessions.len())
            .bold()
            .underline()
    );
    println!();

    for summary in sessions {
        let short_id = &summary.session_id[..8];
        let duration_ms = summary
            .end_time
            .signed_duration_since(summary.start_time)
            .num_milliseconds() as u64;
        let duration_secs = duration_ms as f64 / 1000.0;

        let status = if summary.exit_code == 0 {
            "".green()
        } else {
            "".red()
        };

        let agent = summary.agent_name.as_deref().unwrap_or("unknown");

        println!("{} Session {}", status, short_id.cyan());
        println!("  Agent: {}", agent);
        println!(
            "  Time: {} | Duration: {:.2}s",
            summary.start_time.format("%Y-%m-%d %H:%M"),
            duration_secs
        );
        println!("  Files changed: {}", summary.files_changed);
        println!("  Run: agentlog replay {}", short_id);
        println!();
    }

    println!("{}:", "Quick actions".bold());
    println!("  agentlog replay <session-id>  - View full session details");
    println!("  agentlog export --output session.jsonl  - Export for sharing");

    Ok(())
}

fn parse_time(input: &str) -> Result<chrono::DateTime<Utc>> {
    let now = Utc::now();

    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(input) {
        return Ok(dt.with_timezone(&Utc));
    }

    if let Ok(dt) = chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d") {
        return Ok(chrono::DateTime::from_naive_utc_and_offset(
            dt.and_hms_opt(0, 0, 0).unwrap(),
            Utc,
        ));
    }

    let duration = parse_relative_time(input)?;
    Ok(now - duration)
}

fn parse_relative_time(input: &str) -> Result<Duration> {
    let input = input.trim().to_lowercase();
    let input = input.replace(" ago", "").replace("s", "");

    let (num, unit) = if let Some(pos) = input.find(|c: char| !c.is_ascii_digit() && c != ' ') {
        let num: i64 = input[..pos]
            .trim()
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid number in time expression: {}", input))?;
        (num, input[pos..].trim())
    } else {
        return Err(anyhow::anyhow!("Invalid time format: {}", input));
    };

    let duration = match unit {
        "m" | "min" | "minute" => Duration::minutes(num),
        "h" | "hr" | "hour" => Duration::hours(num),
        "d" | "day" => Duration::days(num),
        "w" | "week" => Duration::weeks(num),
        _ => return Err(anyhow::anyhow!("Unknown time unit: {}", unit)),
    };

    Ok(duration)
}