systemprompt-cli 0.8.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use std::time::Duration;

use anyhow::Result;
use clap::Args;
use serde::Serialize;
use systemprompt_identifiers::UserId;
use systemprompt_oauth::repository::{BridgeSessionRepository, BridgeSessionRow};
use systemprompt_runtime::AppContext;

use crate::CliConfig;
use crate::shared::CommandResult;

const DEFAULT_WITHIN_SECS: u64 = 120;

#[derive(Debug, Args)]
pub struct ListArgs {
    #[arg(long, help = "Filter to a single user")]
    pub user_id: Option<String>,
    #[arg(
        long,
        default_value_t = DEFAULT_WITHIN_SECS,
        help = "Treat sessions with a heartbeat newer than this many seconds as active",
    )]
    pub within_secs: u64,
}

#[derive(Debug, Serialize)]
pub struct BridgeListOutput {
    pub within_secs: u64,
    pub sessions: Vec<BridgeSessionSummary>,
}

#[derive(Debug, Serialize)]
pub struct BridgeSessionSummary {
    pub session_id: String,
    pub user_id: String,
    pub hostname: String,
    pub bridge_version: String,
    pub os: String,
    pub last_heartbeat_at: String,
    pub last_activity_at: Option<String>,
    pub forwarded_total: i64,
}

pub async fn execute(
    args: ListArgs,
    _config: &CliConfig,
) -> Result<CommandResult<BridgeListOutput>> {
    let ctx = AppContext::new().await?;
    let repo = BridgeSessionRepository::new(ctx.db_pool())?;
    let within = Duration::from_secs(args.within_secs);

    let rows = match args
        .user_id
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty())
    {
        Some(user) => {
            repo.list_active_for_user(&UserId::new(user.to_string()), within)
                .await?
        },
        None => repo.list_active(within).await?,
    };

    let summaries = rows.into_iter().map(summary).collect::<Vec<_>>();
    let title = format!(
        "Active bridge sessions ({} total, last {}s)",
        summaries.len(),
        args.within_secs
    );

    let output = BridgeListOutput {
        within_secs: args.within_secs,
        sessions: summaries,
    };

    Ok(CommandResult::text(output).with_title(title))
}

fn summary(row: BridgeSessionRow) -> BridgeSessionSummary {
    BridgeSessionSummary {
        session_id: row.session_id.as_str().to_string(),
        user_id: row.user_id.as_str().to_string(),
        hostname: row.hostname,
        bridge_version: row.bridge_version,
        os: row.os,
        last_heartbeat_at: row.last_heartbeat_at.to_rfc3339(),
        last_activity_at: row.last_activity_at.map(|t| t.to_rfc3339()),
        forwarded_total: row.forwarded_total,
    }
}