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 anyhow::Result;
use clap::Args;
use std::sync::Arc;
use systemprompt_logging::{CliService, ToolExecutionFilter, TraceQueryService};

use super::{ToolExecutionRow, ToolsListOutput};
use crate::CliConfig;
use crate::commands::infrastructure::logs::duration::parse_since;
use crate::shared::{CommandResult, render_result};

#[derive(Debug, Args)]
pub struct ListArgs {
    #[arg(long, help = "Filter by tool name (partial match)")]
    pub name: Option<String>,

    #[arg(long, help = "Filter by server name (partial match)")]
    pub server: Option<String>,

    #[arg(long, help = "Filter by status (success, error, pending)")]
    pub status: Option<String>,

    #[arg(long, help = "Only show executions since duration (e.g., '1h', '7d')")]
    pub since: Option<String>,

    #[arg(long, short = 'n', default_value = "50", help = "Maximum results")]
    pub limit: i64,
}

crate::define_pool_command!(ListArgs => (), with_config);

async fn execute_with_pool_inner(
    args: ListArgs,
    pool: &Arc<sqlx::PgPool>,
    config: &CliConfig,
) -> Result<()> {
    let since_timestamp = parse_since(args.since.as_ref())?;
    let name_pattern = args.name.as_ref().map(|n| format!("%{n}%"));
    let server_pattern = args.server.as_ref().map(|s| format!("%{s}%"));

    let mut filter = ToolExecutionFilter::new(args.limit);
    if let Some(since) = since_timestamp {
        filter = filter.with_since(since);
    }
    if let Some(name) = name_pattern {
        filter = filter.with_name(name);
    }
    if let Some(server) = server_pattern {
        filter = filter.with_server(server);
    }
    if let Some(status) = args.status.clone() {
        filter = filter.with_status(status);
    }

    let service = TraceQueryService::new(Arc::clone(pool));
    let rows = service.list_tool_executions(&filter).await?;

    let executions: Vec<ToolExecutionRow> = rows
        .into_iter()
        .map(|r| ToolExecutionRow {
            timestamp: r.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(),
            trace_id: r.trace_id,
            tool_name: r.tool_name,
            server: r.server_name.unwrap_or_else(|| "unknown".to_string()),
            status: r.status,
            duration_ms: r.execution_time_ms.map(i64::from),
        })
        .collect();

    let output = ToolsListOutput {
        total: executions.len() as u64,
        executions,
    };

    if config.is_json_output() {
        let result = CommandResult::table(output)
            .with_title("MCP Tool Executions")
            .with_columns(vec![
                "timestamp".to_string(),
                "trace_id".to_string(),
                "tool_name".to_string(),
                "server".to_string(),
                "status".to_string(),
                "duration_ms".to_string(),
            ]);
        render_result(&result);
    } else {
        render_tool_list(&output, &args);
    }

    Ok(())
}

fn render_tool_list(output: &ToolsListOutput, args: &ListArgs) {
    CliService::section("MCP Tool Executions");

    if args.name.is_some() || args.server.is_some() || args.status.is_some() || args.since.is_some()
    {
        if let Some(ref name) = args.name {
            CliService::key_value("Tool", name);
        }
        if let Some(ref server) = args.server {
            CliService::key_value("Server", server);
        }
        if let Some(ref status) = args.status {
            CliService::key_value("Status", status);
        }
        if let Some(ref since) = args.since {
            CliService::key_value("Since", since);
        }
    }

    if output.executions.is_empty() {
        CliService::warning("No tool executions found");
        return;
    }

    for exec in &output.executions {
        let duration = exec.duration_ms.map(|d| format!(" ({}ms)", d));
        let line = format!(
            "{} {}/{} [{}]{}  trace:{}",
            exec.timestamp,
            exec.server,
            exec.tool_name,
            exec.status,
            duration.as_deref().unwrap_or(""),
            exec.trace_id
        );
        match exec.status.as_str() {
            "error" | "failed" => CliService::error(&line),
            _ => CliService::info(&line),
        }
    }

    CliService::info(&format!("Total: {} executions", output.total));
}