systemprompt_cli/commands/admin/session/
show.rs1use 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}