systemprompt-cli 0.2.1

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use systemprompt_identifiers::TaskId;
use systemprompt_logging::{
    AiRequestSummary, CliService, ExecutionStepSummary, McpExecutionSummary, TraceEvent,
};

#[derive(Debug)]
pub struct SummaryContext<'a> {
    pub events: &'a [TraceEvent],
    pub first: Option<DateTime<Utc>>,
    pub last: Option<DateTime<Utc>>,
    pub task_id: Option<&'a TaskId>,
    pub ai_summary: &'a AiRequestSummary,
    pub mcp_summary: &'a McpExecutionSummary,
    pub step_summary: &'a ExecutionStepSummary,
}

pub fn print_summary(ctx: &SummaryContext<'_>) {
    CliService::section("Summary");

    if let (Some(first), Some(last)) = (ctx.first, ctx.last) {
        let duration = last.signed_duration_since(first);
        CliService::key_value("  Duration", &format!("{}ms", duration.num_milliseconds()));
    }

    print_event_counts(ctx.events);
    print_ai_summary(ctx.ai_summary);
    print_mcp_summary(ctx.mcp_summary);
    print_step_summary(ctx.step_summary);
    print_trace_context(ctx.events, ctx.task_id);
    print_status(ctx.events);
}

fn print_event_counts(events: &[TraceEvent]) {
    let mut event_counts: HashMap<String, usize> = HashMap::new();
    for event in events {
        *event_counts.entry(event.event_type.clone()).or_insert(0) += 1;
    }

    let mut count_vec: Vec<_> = event_counts.iter().collect();
    count_vec.sort_by_key(|&(k, _)| k);

    let event_parts: Vec<String> = count_vec
        .iter()
        .map(|(k, v)| format!("{} {}", v, k))
        .collect();
    CliService::key_value(
        "  Events",
        &format!("{} ({})", events.len(), event_parts.join(", ")),
    );
}

fn print_ai_summary(ai_summary: &AiRequestSummary) {
    if ai_summary.request_count > 0 {
        CliService::info("  AI Requests:");
        CliService::key_value("     Requests", &ai_summary.request_count.to_string());
        CliService::key_value(
            "     Tokens",
            &format!(
                "{} (in: {}, out: {})",
                ai_summary.total_tokens,
                ai_summary.total_input_tokens,
                ai_summary.total_output_tokens
            ),
        );
        let dollars = ai_summary.total_cost_microdollars as f64 / 1_000_000.0;
        CliService::key_value("     Cost", &format!("${dollars:.6}"));
        CliService::key_value(
            "     Total Latency",
            &format!("{}ms", ai_summary.total_latency_ms),
        );
        if ai_summary.request_count > 0 {
            let avg_latency = ai_summary.total_latency_ms / ai_summary.request_count;
            CliService::key_value("     Avg Latency", &format!("{avg_latency}ms"));
        }
    }
}

fn print_mcp_summary(mcp_summary: &McpExecutionSummary) {
    if mcp_summary.execution_count > 0 {
        CliService::info("  MCP Tool Executions:");
        CliService::key_value("     Executions", &mcp_summary.execution_count.to_string());
        CliService::key_value(
            "     Total Time",
            &format!("{}ms", mcp_summary.total_execution_time_ms),
        );
        if mcp_summary.execution_count > 0 {
            let avg_time = mcp_summary.total_execution_time_ms / mcp_summary.execution_count;
            CliService::key_value("     Avg Time", &format!("{avg_time}ms"));
        }
    }
}

fn print_step_summary(step_summary: &ExecutionStepSummary) {
    if step_summary.total > 0 {
        CliService::info("  Execution Steps:");
        CliService::key_value(
            "     Steps",
            &format!(
                "{} ({} completed, {} failed, {} pending)",
                step_summary.total,
                step_summary.completed,
                step_summary.failed,
                step_summary.pending
            ),
        );
    }
}

fn print_trace_context(events: &[TraceEvent], task_id: Option<&TaskId>) {
    if let Some(task_id) = task_id {
        CliService::key_value(
            "  Task",
            &format!("{} (use: just ai-trace <task_id>)", task_id.as_str()),
        );
    }

    if let Some(session_id) = events.first().and_then(|e| e.session_id.as_ref()) {
        CliService::key_value("  Session", session_id.as_str());
    }

    if let Some(user_id) = events.first().and_then(|e| e.user_id.as_ref()) {
        CliService::key_value("  User", user_id.as_str());
    }
}

fn print_status(events: &[TraceEvent]) {
    let has_errors = events.iter().any(|e| {
        e.details.contains("ERROR")
            || e.details.contains("failed")
            || e.details.contains("(failed)")
    });

    if has_errors {
        CliService::error("  Status: [FAILED]");
    } else {
        CliService::success("  Status: [OK]");
    }
}