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" | "new") => {
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 agent_id: None,
180 }
181}
182
183fn print_session_help() {
184 eprintln!(
185 "\
186lean-ctx session — Session management
187
188Usage:
189 lean-ctx session Show adoption statistics
190 lean-ctx session task <description> Set current task
191 lean-ctx session finding <summary> Record a finding
192 lean-ctx session decision <summary> Record a decision
193 lean-ctx session save Save current session
194 lean-ctx session load [session-id] Load a session (latest if no ID)
195 lean-ctx session status Show session status
196 lean-ctx session reset|new Reset session
197
198Examples:
199 lean-ctx session task \"implement JWT authentication\"
200 lean-ctx session finding \"auth.rs:42 — missing token validation\"
201 lean-ctx session save
202 lean-ctx session load"
203 );
204}
205
206fn cmd_session_legacy() {
207 let history = load_shell_history();
208 let gain = stats::load_stats();
209
210 let compressible_commands = [
211 "git ",
212 "npm ",
213 "yarn ",
214 "pnpm ",
215 "cargo ",
216 "docker ",
217 "kubectl ",
218 "gh ",
219 "pip ",
220 "pip3 ",
221 "eslint",
222 "prettier",
223 "ruff ",
224 "go ",
225 "golangci-lint",
226 "curl ",
227 "wget ",
228 "grep ",
229 "rg ",
230 "find ",
231 "ls ",
232 ];
233
234 let mut total = 0u32;
235 let mut via_hook = 0u32;
236
237 for line in &history {
238 let cmd = line.trim().to_lowercase();
239 if cmd.starts_with("lean-ctx") {
240 via_hook += 1;
241 total += 1;
242 } else {
243 for p in &compressible_commands {
244 if cmd.starts_with(p) {
245 total += 1;
246 break;
247 }
248 }
249 }
250 }
251
252 let pct = if total > 0 {
253 (via_hook as f64 / total as f64 * 100.0).round() as u32
254 } else {
255 0
256 };
257
258 println!("lean-ctx session statistics\n");
259 println!("Adoption: {pct}% ({via_hook}/{total} compressible commands)");
260 println!("Saved: {} tokens total", gain.total_saved);
261 println!("Calls: {} compressed", gain.total_calls);
262
263 if total > via_hook {
264 let missed = total - via_hook;
265 let est = missed * 150;
266 println!("Missed: {missed} commands (~{est} tokens saveable)");
267 }
268
269 println!("\nRun 'lean-ctx discover' for details on missed commands.");
270}
271
272pub fn cmd_wrapped(args: &[String]) {
273 let period = if args.iter().any(|a| a == "--month") {
274 "month"
275 } else if args.iter().any(|a| a == "--all") {
276 "all"
277 } else {
278 "week"
279 };
280
281 eprintln!("[DEPRECATED] Use `lean-ctx gain --wrapped`.");
282 println!(
283 "{}",
284 crate::tools::ctx_gain::handle("wrapped", Some(period), None, None)
285 );
286}
287
288pub fn cmd_sessions(args: &[String]) {
289 use crate::core::session::SessionState;
290
291 let action = args.first().map_or("list", std::string::String::as_str);
292
293 match action {
294 "list" | "ls" => {
295 let sessions = SessionState::list_sessions();
296 if sessions.is_empty() {
297 println!("No sessions found.");
298 return;
299 }
300 println!("Sessions ({}):\n", sessions.len());
301 for s in sessions.iter().take(20) {
302 let task = s.task.as_deref().unwrap_or("(no task)");
303 let task_short: String = task.chars().take(50).collect();
304 let date = s.updated_at.format("%Y-%m-%d %H:%M");
305 println!(
306 " {} | v{:3} | {:5} calls | {:>8} tok | {} | {}",
307 s.id,
308 s.version,
309 s.tool_calls,
310 format_tokens_cli(s.tokens_saved),
311 date,
312 task_short
313 );
314 }
315 if sessions.len() > 20 {
316 println!(" ... +{} more", sessions.len() - 20);
317 }
318 }
319 "show" => {
320 let id = args.get(1);
321 let session = if let Some(id) = id {
326 SessionState::load_by_id(id)
327 } else {
328 SessionState::load_latest().or_else(SessionState::load_global_latest_pointer)
329 };
330 match session {
331 Some(s) => println!("{}", s.format_compact()),
332 None => println!("Session not found."),
333 }
334 }
335 "cleanup" => {
336 let days = args.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(7);
337 let removed = SessionState::cleanup_old_sessions(days);
338 let (wf_removed, wf_freed) = crate::core::workflow::cleanup_expired();
339 println!("Cleaned up {removed} session(s) older than {days} days.");
340 if wf_removed > 0 {
341 println!(
342 "Cleaned up {wf_removed} expired workflow file(s) ({:.1} KB freed).",
343 wf_freed as f64 / 1024.0
344 );
345 }
346 }
347 "delete" | "rm" => {
348 let Some(id) = args.get(1) else {
349 eprintln!("Usage: lean-ctx sessions delete <id>");
350 std::process::exit(1);
351 };
352 match SessionState::delete_session(id) {
353 Ok(true) => println!("Deleted session {id}."),
354 Ok(false) => {
355 eprintln!("Session not found: {id}");
356 std::process::exit(1);
357 }
358 Err(e) => {
359 eprintln!("Failed to delete session {id}: {e}");
360 std::process::exit(1);
361 }
362 }
363 }
364 "doctor" => {
365 let apply = args.iter().any(|a| a == "--apply" || a == "--fix");
366 let (found, quarantined) = SessionState::doctor_quarantine_unsafe_roots(apply);
367 if found.is_empty() {
368 println!("session doctor: no contaminated sessions found.");
369 } else {
370 println!(
371 "session doctor: {} session(s) rooted at a broad/unsafe path (HOME/'/'/agent dir):",
372 found.len()
373 );
374 for (id, root) in &found {
375 println!(" {id} | root: {root}");
376 }
377 if apply {
378 println!("\nQuarantined {quarantined} session(s) to sessions/quarantine/.");
379 } else {
380 println!("\nRun `lean-ctx sessions doctor --apply` to quarantine them.");
381 }
382 }
383 }
384 _ => {
385 eprintln!(
386 "Usage: lean-ctx sessions [list|show [id]|delete <id>|cleanup [days]|doctor [--apply]]"
387 );
388 std::process::exit(1);
389 }
390 }
391}