1use crate::core::session::SessionState;
2use crate::core::stats;
3use crate::tools::ctx_session::{self, SessionToolOptions};
4
5use super::common::{format_tokens_cli, load_shell_history};
6
7pub fn cmd_session_action(args: &[String]) {
8 let action = args.first().map(String::as_str);
9
10 match action {
11 Some("task") => {
12 let desc = args.get(1).map_or("(no description)", String::as_str);
13 #[cfg(unix)]
14 {
15 #[cfg(unix)]
16 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
17 "ctx_session",
18 Some(serde_json::json!({ "action": "task", "value": desc })),
19 ) {
20 println!("{out}");
21 return;
22 }
23 }
24 let mut session = load_or_create_session();
25 let out =
26 ctx_session::handle(&mut session, &[], "task", Some(desc), None, default_opts());
27 let _ = session.save();
28 println!("{out}");
29 }
30 Some("finding") => {
31 let summary = args.get(1).map_or("(no summary)", String::as_str);
32 #[cfg(unix)]
33 {
34 #[cfg(unix)]
35 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
36 "ctx_session",
37 Some(serde_json::json!({ "action": "finding", "value": summary })),
38 ) {
39 println!("{out}");
40 return;
41 }
42 }
43 let mut session = load_or_create_session();
44 let out = ctx_session::handle(
45 &mut session,
46 &[],
47 "finding",
48 Some(summary),
49 None,
50 default_opts(),
51 );
52 let _ = session.save();
53 println!("{out}");
54 }
55 Some("save") => {
56 #[cfg(unix)]
57 {
58 #[cfg(unix)]
59 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
60 "ctx_session",
61 Some(serde_json::json!({ "action": "save" })),
62 ) {
63 println!("{out}");
64 return;
65 }
66 }
67 let mut session = load_or_create_session();
68 let out = ctx_session::handle(&mut session, &[], "save", None, None, default_opts());
69 println!("{out}");
70 }
71 Some("load") => {
72 let id = args.get(1).map(String::as_str);
73 #[cfg(unix)]
74 {
75 #[cfg(unix)]
76 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
77 "ctx_session",
78 Some(serde_json::json!({ "action": "load", "session_id": id })),
79 ) {
80 println!("{out}");
81 return;
82 }
83 }
84 let mut session = SessionState::new();
85 let out = ctx_session::handle(&mut session, &[], "load", None, id, default_opts());
86 println!("{out}");
87 }
88 Some("status") => {
89 #[cfg(unix)]
90 {
91 #[cfg(unix)]
92 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
93 "ctx_session",
94 Some(serde_json::json!({ "action": "status" })),
95 ) {
96 println!("{out}");
97 return;
98 }
99 }
100 let mut session = load_or_create_session();
101 let out = ctx_session::handle(&mut session, &[], "status", None, None, default_opts());
102 println!("{out}");
103 }
104 Some("decision") => {
105 let desc = args.get(1).map_or("(no description)", String::as_str);
106 #[cfg(unix)]
107 {
108 #[cfg(unix)]
109 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
110 "ctx_session",
111 Some(serde_json::json!({ "action": "decision", "value": desc })),
112 ) {
113 println!("{out}");
114 return;
115 }
116 }
117 let mut session = load_or_create_session();
118 let out = ctx_session::handle(
119 &mut session,
120 &[],
121 "decision",
122 Some(desc),
123 None,
124 default_opts(),
125 );
126 let _ = session.save();
127 println!("{out}");
128 }
129 Some("reset") => {
130 #[cfg(unix)]
131 {
132 #[cfg(unix)]
133 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
134 "ctx_session",
135 Some(serde_json::json!({ "action": "reset" })),
136 ) {
137 println!("{out}");
138 return;
139 }
140 }
141 let mut session = load_or_create_session();
142 let out = ctx_session::handle(&mut session, &[], "reset", None, None, default_opts());
143 println!("{out}");
144 }
145 None => {
146 cmd_session_legacy();
147 }
148 Some(other) => {
149 eprintln!("Unknown session action: {other}");
150 print_session_help();
151 std::process::exit(1);
152 }
153 }
154}
155
156fn load_or_create_session() -> SessionState {
157 let mut session = SessionState::load_latest().unwrap_or_default();
158 if session.project_root.is_none()
164 && let Ok(cwd) = std::env::current_dir()
165 && !crate::core::pathutil::is_broad_or_unsafe_root(&cwd)
166 {
167 session.project_root = Some(cwd.to_string_lossy().to_string());
168 }
169 session
170}
171
172fn default_opts() -> SessionToolOptions<'static> {
173 SessionToolOptions {
174 format: None,
175 path: None,
176 write: false,
177 privacy: None,
178 terse: None,
179 }
180}
181
182fn print_session_help() {
183 eprintln!(
184 "\
185lean-ctx session — Session management
186
187Usage:
188 lean-ctx session Show adoption statistics
189 lean-ctx session task <description> Set current task
190 lean-ctx session finding <summary> Record a finding
191 lean-ctx session decision <summary> Record a decision
192 lean-ctx session save Save current session
193 lean-ctx session load [session-id] Load a session (latest if no ID)
194 lean-ctx session status Show session status
195 lean-ctx session reset Reset session
196
197Examples:
198 lean-ctx session task \"implement JWT authentication\"
199 lean-ctx session finding \"auth.rs:42 — missing token validation\"
200 lean-ctx session save
201 lean-ctx session load"
202 );
203}
204
205fn cmd_session_legacy() {
206 let history = load_shell_history();
207 let gain = stats::load_stats();
208
209 let compressible_commands = [
210 "git ",
211 "npm ",
212 "yarn ",
213 "pnpm ",
214 "cargo ",
215 "docker ",
216 "kubectl ",
217 "gh ",
218 "pip ",
219 "pip3 ",
220 "eslint",
221 "prettier",
222 "ruff ",
223 "go ",
224 "golangci-lint",
225 "curl ",
226 "wget ",
227 "grep ",
228 "rg ",
229 "find ",
230 "ls ",
231 ];
232
233 let mut total = 0u32;
234 let mut via_hook = 0u32;
235
236 for line in &history {
237 let cmd = line.trim().to_lowercase();
238 if cmd.starts_with("lean-ctx") {
239 via_hook += 1;
240 total += 1;
241 } else {
242 for p in &compressible_commands {
243 if cmd.starts_with(p) {
244 total += 1;
245 break;
246 }
247 }
248 }
249 }
250
251 let pct = if total > 0 {
252 (via_hook as f64 / total as f64 * 100.0).round() as u32
253 } else {
254 0
255 };
256
257 println!("lean-ctx session statistics\n");
258 println!("Adoption: {pct}% ({via_hook}/{total} compressible commands)");
259 println!("Saved: {} tokens total", gain.total_saved);
260 println!("Calls: {} compressed", gain.total_calls);
261
262 if total > via_hook {
263 let missed = total - via_hook;
264 let est = missed * 150;
265 println!("Missed: {missed} commands (~{est} tokens saveable)");
266 }
267
268 println!("\nRun 'lean-ctx discover' for details on missed commands.");
269}
270
271pub fn cmd_wrapped(args: &[String]) {
272 let period = if args.iter().any(|a| a == "--month") {
273 "month"
274 } else if args.iter().any(|a| a == "--all") {
275 "all"
276 } else {
277 "week"
278 };
279
280 eprintln!("[DEPRECATED] Use `lean-ctx gain --wrapped`.");
281 println!(
282 "{}",
283 crate::tools::ctx_gain::handle("wrapped", Some(period), None, None)
284 );
285}
286
287pub fn cmd_sessions(args: &[String]) {
288 use crate::core::session::SessionState;
289
290 let action = args.first().map_or("list", std::string::String::as_str);
291
292 match action {
293 "list" | "ls" => {
294 let sessions = SessionState::list_sessions();
295 if sessions.is_empty() {
296 println!("No sessions found.");
297 return;
298 }
299 println!("Sessions ({}):\n", sessions.len());
300 for s in sessions.iter().take(20) {
301 let task = s.task.as_deref().unwrap_or("(no task)");
302 let task_short: String = task.chars().take(50).collect();
303 let date = s.updated_at.format("%Y-%m-%d %H:%M");
304 println!(
305 " {} | v{:3} | {:5} calls | {:>8} tok | {} | {}",
306 s.id,
307 s.version,
308 s.tool_calls,
309 format_tokens_cli(s.tokens_saved),
310 date,
311 task_short
312 );
313 }
314 if sessions.len() > 20 {
315 println!(" ... +{} more", sessions.len() - 20);
316 }
317 }
318 "show" => {
319 let id = args.get(1);
320 let session = if let Some(id) = id {
325 SessionState::load_by_id(id)
326 } else {
327 SessionState::load_latest().or_else(SessionState::load_global_latest_pointer)
328 };
329 match session {
330 Some(s) => println!("{}", s.format_compact()),
331 None => println!("Session not found."),
332 }
333 }
334 "cleanup" => {
335 let days = args.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(7);
336 let removed = SessionState::cleanup_old_sessions(days);
337 let (wf_removed, wf_freed) = crate::core::workflow::cleanup_expired();
338 println!("Cleaned up {removed} session(s) older than {days} days.");
339 if wf_removed > 0 {
340 println!(
341 "Cleaned up {wf_removed} expired workflow file(s) ({:.1} KB freed).",
342 wf_freed as f64 / 1024.0
343 );
344 }
345 }
346 "delete" | "rm" => {
347 let Some(id) = args.get(1) else {
348 eprintln!("Usage: lean-ctx sessions delete <id>");
349 std::process::exit(1);
350 };
351 match SessionState::delete_session(id) {
352 Ok(true) => println!("Deleted session {id}."),
353 Ok(false) => {
354 eprintln!("Session not found: {id}");
355 std::process::exit(1);
356 }
357 Err(e) => {
358 eprintln!("Failed to delete session {id}: {e}");
359 std::process::exit(1);
360 }
361 }
362 }
363 "doctor" => {
364 let apply = args.iter().any(|a| a == "--apply" || a == "--fix");
365 let (found, quarantined) = SessionState::doctor_quarantine_unsafe_roots(apply);
366 if found.is_empty() {
367 println!("session doctor: no contaminated sessions found.");
368 } else {
369 println!(
370 "session doctor: {} session(s) rooted at a broad/unsafe path (HOME/'/'/agent dir):",
371 found.len()
372 );
373 for (id, root) in &found {
374 println!(" {id} | root: {root}");
375 }
376 if apply {
377 println!("\nQuarantined {quarantined} session(s) to sessions/quarantine/.");
378 } else {
379 println!("\nRun `lean-ctx sessions doctor --apply` to quarantine them.");
380 }
381 }
382 }
383 _ => {
384 eprintln!(
385 "Usage: lean-ctx sessions [list|show [id]|delete <id>|cleanup [days]|doctor [--apply]]"
386 );
387 std::process::exit(1);
388 }
389 }
390}