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 systemprompt_identifiers::{ContextId, TaskId};
use systemprompt_logging::{
    AiRequestInfo, AiTraceService, CliService, McpToolExecution, ToolLogEntry,
};

use super::ai_display::{ToolCallRow, print_content_block, print_section, truncate};
use tabled::Table;
use tabled::settings::Style;

pub async fn print_mcp_executions(
    service: &AiTraceService,
    executions: &[McpToolExecution],
    task_id: &TaskId,
    context_id: &ContextId,
    show_full: bool,
) {
    if executions.is_empty() {
        print_tool_errors_from_logs(service, task_id, context_id).await;
        return;
    }

    let tool_rows: Vec<ToolCallRow> = executions
        .iter()
        .map(|e| ToolCallRow {
            tool_name: e.tool_name.clone(),
            server: e.server_name.clone(),
            status: e.status.clone(),
            duration: e
                .execution_time_ms
                .map_or_else(|| "-".to_string(), |ms| format!("{}ms", ms)),
        })
        .collect();

    print_section("MCP TOOL EXECUTIONS");
    let table = Table::new(tool_rows).with(Style::rounded()).to_string();
    CliService::info(&table);

    for exec in executions {
        if exec.status == "failed" {
            if let Some(ref error) = exec.error_message {
                CliService::error(&format!("  {} failed:", exec.tool_name));
                print_content_block(error);
            }
        }

        print_tool_io(exec, show_full);

        if let Ok(linked_requests) = service
            .get_mcp_linked_ai_requests(exec.mcp_execution_id.as_str())
            .await
        {
            if !linked_requests.is_empty() {
                print_mcp_linked_ai_requests(service, &linked_requests, &exec.tool_name).await;
            }
        }
    }
}

fn print_tool_io(exec: &McpToolExecution, show_full: bool) {
    let has_input = !exec.input.is_empty();
    let has_output = exec.output.as_ref().is_some_and(|s| !s.is_empty());

    if has_input || has_output {
        CliService::info(&format!("{}:", exec.tool_name));

        if has_input {
            CliService::info("    input:");
            if show_full || exec.input.len() <= 200 {
                print_tool_content(&exec.input);
            } else {
                print_tool_content(&truncate(&exec.input, 200));
            }
        }

        if let Some(ref output_str) = exec.output {
            if !output_str.is_empty() {
                CliService::info("    result:");
                if show_full || output_str.len() <= 500 {
                    print_tool_content(output_str);
                } else {
                    print_tool_content(&truncate(output_str, 500));
                    CliService::info("    [Truncated - use --tool-results for full output]");
                }
            }
        }
    }
}

fn print_tool_content(content: &str) {
    if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
        if let Ok(pretty) = serde_json::to_string_pretty(&json) {
            for line in pretty.lines() {
                CliService::info(&format!("      {line}"));
            }
            return;
        }
    }
    for line in content.lines() {
        CliService::info(&format!("      {line}"));
    }
}

async fn print_mcp_linked_ai_requests(
    service: &AiTraceService,
    requests: &[AiRequestInfo],
    tool_name: &str,
) {
    CliService::info(&format!("  → AI requests made by {tool_name}:"));

    for req in requests {
        let tokens = req.input_tokens.unwrap_or(0) + req.output_tokens.unwrap_or(0);
        let latency_str = req
            .latency_ms
            .map_or_else(|| "-".to_string(), |ms| format!("{ms}ms"));

        CliService::info(&format!(
            "    {} {}/{} | {tokens} tokens | {latency_str}",
            truncate(req.id.as_str(), 8),
            req.provider,
            req.model
        ));

        if let Ok(previews) = service
            .get_ai_request_message_previews(req.id.as_str())
            .await
        {
            for msg in previews {
                let preview = if msg.content.len() >= 500 {
                    format!("{}...", truncate(&msg.content, 200))
                } else if msg.role == "system" && msg.content.len() > 100 {
                    format!("[System: {} chars]", msg.content.len())
                } else {
                    truncate(&msg.content, 200)
                };

                CliService::info(&format!(
                    "      #{} [{}] {preview}",
                    msg.sequence_number,
                    msg.role.to_uppercase()
                ));
            }
        }
    }
}

async fn print_tool_errors_from_logs(
    service: &AiTraceService,
    task_id: &TaskId,
    context_id: &ContextId,
) {
    let Ok(logs) = service
        .get_tool_logs(task_id.as_str(), context_id.as_str())
        .await
    else {
        return;
    };

    if logs.is_empty() {
        return;
    }

    print_section("TOOL EXECUTION LOGS");
    CliService::info("  (MCP execution records not found - showing logs)");

    let mut has_errors = false;
    for log in &logs {
        let time_str = log.timestamp.format("%H:%M:%S%.3f").to_string();

        let level_symbol = match log.level.as_str() {
            "ERROR" => {
                has_errors = true;
                ""
            },
            "WARN" => "",
            "DEBUG" => "·",
            _ => "",
        };

        let log_line = format!(
            "  {time_str} {level_symbol} [{}] {}",
            log.module,
            truncate(&log.message, 100)
        );

        match log.level.as_str() {
            "ERROR" => CliService::error(&log_line),
            "WARN" => CliService::warning(&log_line),
            _ => CliService::info(&log_line),
        }
    }

    if has_errors {
        print_error_details(&logs);
    }
}

fn print_error_details(logs: &[ToolLogEntry]) {
    CliService::error("  Tool Errors:");
    for log in logs {
        if log.level == "ERROR" {
            CliService::error(&format!("    {}: error:", log.module));
            print_content_block(&format!("      {}", log.message));
        }
    }
}