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