carryctx 0.3.0

Persistent project context for coding agents
use crate::*;
use carryctx::adapter::unit_of_work::UnitOfWork;
use carryctx::application;
use carryctx::application::runtime::InvocationContext;
use carryctx::error::{CarryCtxError, ExitCode};
use clap::Parser;

// ── Event ────────────────────────────────────────────────────────────────

#[derive(Parser, Debug)]
pub enum EventCommand {
    /// List events matching the specified filters
    List {
        /// Filter by associated task ULID
        #[arg(long)]
        task: Option<String>,
        /// Filter by associated agent ULID
        #[arg(long)]
        agent: Option<String>,
        /// Filter by associated session ULID
        #[arg(long)]
        session: Option<String>,
        /// Filter by event type (e.g., TaskTransition, SessionStarted)
        #[arg(long)]
        event_type: Option<String>,
        /// Only show events after this timestamp or relative duration
        #[arg(long)]
        since: Option<String>,
        /// Only show events before this timestamp or relative duration
        #[arg(long)]
        until: Option<String>,
        /// Limit the number of returned events
        #[arg(long)]
        limit: Option<u64>,
    },
    /// Show full raw JSON details for a specific event ULID
    Show { event_id: String },
}

#[derive(Parser, Debug)]
pub struct EventArgs {
    /// Event subcommand to execute
    #[command(subcommand)]
    pub command: EventCommand,
}

// ═══════════════════════════════════════════════════════════════════════════
//  Handler: event
// ═══════════════════════════════════════════════════════════════════════════

pub fn handle_event(
    args: &EventArgs,
    ctx: &InvocationContext,
    is_json: bool,
) -> Result<ExitCode, ExitCode> {
    let mut runtime = try_open_runtime(ctx)?;
    let project_id = &runtime.config.project.id;
    let conn = runtime.database.connection_mut();

    match &args.command {
        EventCommand::List {
            task,
            agent,
            session,
            event_type,
            since,
            until,
            limit,
        } => {
            // Resolve agent reference (name or ULID) to ULID for filtering.
            // The local --agent clashes with the global --agent (CARRYCTX_AGENT env),
            // so resolve it here to avoid filtering by raw agent name.
            // Resolve agent reference (name or ULID) to ULID for filtering.
            let resolved_agent_id = agent.as_deref().and_then(|a| {
                if a.is_empty() {
                    None
                } else {
                    resolve_agent_id(project_id, a, conn).ok()
                }
            });
            // Resolve task reference (display ID or ULID) to ULID for filtering.
            let resolved_task_id = task.as_deref().and_then(|t| {
                if t.is_empty() {
                    None
                } else {
                    resolve_task_id(project_id, t, conn).ok()
                }
            });
            let filter = EventFilter {
                project_id: project_id.to_string(),
                task_id: resolved_task_id,
                agent_id: resolved_agent_id,
                session_id: session.clone(),
                event_type: event_type.clone(),
                since: since.clone(),
                until: until.clone(),
                limit: *limit,
            };
            let repo = carryctx::adapter::sqlite_repos::SqliteEventRepository::new(conn);
            let events = repo.list(&filter).map_err(|e| e.exit_code)?;

            // Markdown format support
            if ctx.format == carryctx::application::runtime::OutputFormat::Markdown {
                let mut out = String::from("# Events\n\n");
                out.push_str("| Type | Agent | Occurred At |\n");
                out.push_str("|---|---|---|\n");
                for e in &events {
                    let agent = e.actor_agent_id.as_deref().unwrap_or("-");
                    let agent_short = if agent.len() > 8 { &agent[..8] } else { agent };
                    out.push_str(&format!(
                        "| {} | {} | {} |\n",
                        e.event_type,
                        agent_short,
                        &e.occurred_at[..19]
                    ));
                }
                if !ctx.quiet {
                    print!("{out}");
                }
                return Ok(ExitCode::Success);
            }

            let result = serde_json::json!({"events": events, "next_cursor": null});
            render_and_print("event.list", Ok(result), is_json, ctx.quiet)
        }
        EventCommand::Show { event_id } => {
            let tx = conn
                .transaction()
                .map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
            let uow = UnitOfWork::new(tx);
            let result = application::event::show_event(project_id, event_id, &uow);
            render_and_print("event.show", result, is_json, ctx.quiet)
        }
    }
}