Skip to main content

systemprompt_cli/commands/admin/session/
show.rs

1//! `admin session show` command listing sessions and routing state.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_cloud::{CliSession, LOCAL_SESSION_KEY, SessionKey, SessionStore, TenantStore};
7
8use super::types::{RoutingInfo, SessionInfo, SessionShowOutput};
9use crate::CliConfig;
10use crate::paths::ResolvedPaths;
11use crate::shared::CommandOutput;
12
13pub(super) fn execute(_config: &CliConfig) -> CommandOutput {
14    let paths = ResolvedPaths::discover();
15
16    let sessions = collect_sessions(&paths);
17    let routing = collect_routing_info(&paths);
18
19    let output = SessionShowOutput { sessions, routing };
20
21    CommandOutput::card_value("Session Info", &output)
22}
23
24fn collect_sessions(paths: &ResolvedPaths) -> Vec<SessionInfo> {
25    let sessions_dir = paths.sessions_dir();
26
27    let Ok(store) = SessionStore::load_or_create(&sessions_dir) else {
28        return Vec::new();
29    };
30
31    let active_key = store.active_key.clone();
32    let active_profile = store.active_profile_name.clone();
33
34    let mut results = Vec::new();
35    let mut displayed_active = false;
36
37    for (key, session) in store.all_sessions() {
38        let is_active = active_key.as_ref() == Some(key);
39        if is_active {
40            displayed_active = true;
41        }
42        results.push(session_info(key, session, is_active));
43    }
44
45    if !displayed_active && (active_key.is_some() || active_profile.is_some()) {
46        results.push(missing_active_session(
47            active_key.as_deref(),
48            active_profile.as_deref(),
49        ));
50    }
51
52    results
53}
54
55pub fn session_info(key: &str, session: &CliSession, is_active: bool) -> SessionInfo {
56    let display_key = if key == LOCAL_SESSION_KEY {
57        "local".to_owned()
58    } else {
59        key.strip_prefix("tenant_")
60            .map_or_else(|| key.to_owned(), String::from)
61    };
62
63    let expires_in = if session.is_expired() {
64        None
65    } else {
66        let remaining = session.expires_at - chrono::Utc::now();
67        let hours = remaining.num_hours();
68        let minutes = remaining.num_minutes() % 60;
69        Some(format!("{}h {}m", hours, minutes))
70    };
71
72    let stale_warning = {
73        let context_age = chrono::Utc::now() - session.last_used;
74        if context_age.num_hours() > 24 {
75            Some(format!(
76                "Context may be stale (last used {}h ago). Re-login with --force-new if commands \
77                 fail.",
78                context_age.num_hours()
79            ))
80        } else {
81            None
82        }
83    };
84
85    SessionInfo {
86        key: display_key,
87        profile_name: session.profile_name.as_str().to_owned(),
88        user_email: session.user_email.as_str().to_owned(),
89        session_id: Some(session.session_id.clone()),
90        context_id: Some(session.context_id.clone()),
91        is_active,
92        is_expired: session.is_expired(),
93        expires_in,
94        stale_warning,
95    }
96}
97
98pub fn missing_active_session(
99    active_key: Option<&str>,
100    active_profile: Option<&str>,
101) -> SessionInfo {
102    let display_name = active_profile.unwrap_or_else(|| {
103        active_key.map_or("unknown", |k| {
104            if k == LOCAL_SESSION_KEY {
105                "local"
106            } else {
107                k.strip_prefix("tenant_").unwrap_or(k)
108            }
109        })
110    });
111
112    SessionInfo {
113        key: display_name.to_owned(),
114        profile_name: display_name.to_owned(),
115        user_email: String::new(),
116        session_id: None,
117        context_id: None,
118        is_active: true,
119        is_expired: false,
120        expires_in: None,
121        stale_warning: Some(
122            "No session. Run 'systemprompt admin session login' to create a session.".to_owned(),
123        ),
124    }
125}
126
127fn collect_routing_info(paths: &ResolvedPaths) -> Option<RoutingInfo> {
128    let sessions_dir = paths.sessions_dir();
129    let store = SessionStore::load_or_create(&sessions_dir).ok()?;
130    let active_key = store.active_session_key()?;
131
132    let session = store.sessions.get(&active_key.as_storage_key());
133
134    let profile_name = session
135        .map(|s| s.profile_name.as_str().to_owned())
136        .or_else(|| store.active_profile_name.clone())
137        .unwrap_or_else(|| "unknown".to_owned());
138
139    match &active_key {
140        SessionKey::Local => Some(RoutingInfo {
141            profile_name,
142            target: "Local".to_owned(),
143            tenant: None,
144            hostname: None,
145        }),
146        SessionKey::Tenant(tenant_id) => {
147            let hostname = resolve_remote_hostname(paths, tenant_id.as_str());
148            Some(RoutingInfo {
149                profile_name,
150                target: if hostname.is_some() {
151                    "Remote".to_owned()
152                } else {
153                    "Tenant".to_owned()
154                },
155                tenant: Some(tenant_id.as_str().to_owned()),
156                hostname,
157            })
158        },
159    }
160}
161
162fn resolve_remote_hostname(paths: &ResolvedPaths, tenant: &str) -> Option<String> {
163    let tenants_path = paths.tenants_path();
164    let store = TenantStore::load_from_path(&tenants_path).ok()?;
165    let tenant = store.find_tenant(&systemprompt_identifiers::TenantId::new(tenant))?;
166    tenant.hostname.clone()
167}