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