systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::{Result, anyhow};
use clap::Args;
use std::path::Path;
use systemprompt_models::AGENT_CONFIG_FILENAME;

use crate::CliConfig;
use crate::shared::{CommandResult, truncate_with_ellipsis};

use super::types::{
    AgentDetailOutput, AgentListOutput, AgentSummary, ListOrDetail, get_agents_path,
    parse_agent_from_config,
};

#[derive(Debug, Clone, Args)]
pub struct ListArgs {
    #[arg(help = "Agent ID to show details (optional)")]
    pub name: Option<String>,

    #[arg(long, help = "Show only enabled agents")]
    pub enabled: bool,

    #[arg(long, help = "Show only disabled agents", conflicts_with = "enabled")]
    pub disabled: bool,
}

pub fn execute(args: ListArgs, _config: &CliConfig) -> Result<CommandResult<ListOrDetail>> {
    let agents_path = get_agents_path()?;

    if let Some(name) = args.name {
        return show_agent_detail(&name, &agents_path);
    }

    let agents = scan_agents(&agents_path)?;

    let filtered: Vec<AgentSummary> = agents
        .into_iter()
        .filter(|a| {
            if args.enabled {
                a.enabled
            } else if args.disabled {
                !a.enabled
            } else {
                true
            }
        })
        .collect();

    let output = AgentListOutput { agents: filtered };

    Ok(CommandResult::table(ListOrDetail::List(output))
        .with_title("Agents")
        .with_columns(vec![
            "agent_id".to_string(),
            "name".to_string(),
            "display_name".to_string(),
            "enabled".to_string(),
            "port".to_string(),
            "tags".to_string(),
        ]))
}

fn show_agent_detail(agent_id: &str, agents_path: &Path) -> Result<CommandResult<ListOrDetail>> {
    let agent_dir = agents_path.join(agent_id);

    if !agent_dir.exists() {
        return Err(anyhow!("Agent '{}' not found", agent_id));
    }

    let config_path = agent_dir.join(AGENT_CONFIG_FILENAME);

    if !config_path.exists() {
        return Err(anyhow!(
            "Agent '{}' has no {} file",
            agent_id,
            AGENT_CONFIG_FILENAME
        ));
    }

    let parsed = parse_agent_from_config(&config_path, &agent_dir)?;

    let system_prompt_preview = parsed
        .system_prompt
        .as_deref()
        .map_or_else(String::new, |s| truncate_with_ellipsis(s, 200));

    let output = AgentDetailOutput {
        agent_id: agent_id.to_string(),
        name: parsed.name,
        display_name: parsed.display_name,
        description: parsed.description,
        enabled: parsed.enabled,
        port: parsed.port,
        tags: parsed.tags,
        category: parsed.category,
        mcp_servers: parsed.mcp_servers,
        skills: parsed.skills,
        system_prompt_preview,
    };

    Ok(CommandResult::card(ListOrDetail::Detail(output)).with_title(format!("Agent: {agent_id}")))
}

fn scan_agents(agents_path: &Path) -> Result<Vec<AgentSummary>> {
    if !agents_path.exists() {
        return Ok(Vec::new());
    }

    let mut agents = Vec::new();

    for entry in std::fs::read_dir(agents_path)? {
        let entry = entry?;
        let agent_path = entry.path();

        if !agent_path.is_dir() {
            continue;
        }

        let config_path = agent_path.join(AGENT_CONFIG_FILENAME);
        if !config_path.exists() {
            continue;
        }

        match parse_agent_from_config(&config_path, &agent_path) {
            Ok(parsed) => {
                let dir_name = agent_path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .ok_or_else(|| anyhow!("Invalid agent directory name"))?;

                agents.push(AgentSummary {
                    agent_id: dir_name.to_string(),
                    name: parsed.name,
                    display_name: parsed.display_name,
                    enabled: parsed.enabled,
                    port: parsed.port,
                    tags: parsed.tags,
                });
            },
            Err(e) => {
                tracing::warn!(
                    path = %agent_path.display(),
                    error = %e,
                    "Failed to parse agent"
                );
            },
        }
    }

    agents.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
    Ok(agents)
}