Skip to main content

lean_ctx/cli/
session_cmd.rs

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    SessionState::load_latest().unwrap_or_default()
158}
159
160fn default_opts() -> SessionToolOptions<'static> {
161    SessionToolOptions {
162        format: None,
163        path: None,
164        write: false,
165        privacy: None,
166        terse: None,
167    }
168}
169
170fn print_session_help() {
171    eprintln!(
172        "\
173lean-ctx session — Session management
174
175Usage:
176  lean-ctx session                      Show adoption statistics
177  lean-ctx session task <description>   Set current task
178  lean-ctx session finding <summary>    Record a finding
179  lean-ctx session decision <summary>   Record a decision
180  lean-ctx session save                 Save current session
181  lean-ctx session load [session-id]    Load a session (latest if no ID)
182  lean-ctx session status               Show session status
183  lean-ctx session reset                Reset session
184
185Examples:
186  lean-ctx session task \"implement JWT authentication\"
187  lean-ctx session finding \"auth.rs:42 — missing token validation\"
188  lean-ctx session save
189  lean-ctx session load"
190    );
191}
192
193fn cmd_session_legacy() {
194    let history = load_shell_history();
195    let gain = stats::load_stats();
196
197    let compressible_commands = [
198        "git ",
199        "npm ",
200        "yarn ",
201        "pnpm ",
202        "cargo ",
203        "docker ",
204        "kubectl ",
205        "gh ",
206        "pip ",
207        "pip3 ",
208        "eslint",
209        "prettier",
210        "ruff ",
211        "go ",
212        "golangci-lint",
213        "curl ",
214        "wget ",
215        "grep ",
216        "rg ",
217        "find ",
218        "ls ",
219    ];
220
221    let mut total = 0u32;
222    let mut via_hook = 0u32;
223
224    for line in &history {
225        let cmd = line.trim().to_lowercase();
226        if cmd.starts_with("lean-ctx") {
227            via_hook += 1;
228            total += 1;
229        } else {
230            for p in &compressible_commands {
231                if cmd.starts_with(p) {
232                    total += 1;
233                    break;
234                }
235            }
236        }
237    }
238
239    let pct = if total > 0 {
240        (via_hook as f64 / total as f64 * 100.0).round() as u32
241    } else {
242        0
243    };
244
245    println!("lean-ctx session statistics\n");
246    println!("Adoption:    {pct}% ({via_hook}/{total} compressible commands)");
247    println!("Saved:       {} tokens total", gain.total_saved);
248    println!("Calls:       {} compressed", gain.total_calls);
249
250    if total > via_hook {
251        let missed = total - via_hook;
252        let est = missed * 150;
253        println!("Missed:      {missed} commands (~{est} tokens saveable)");
254    }
255
256    println!("\nRun 'lean-ctx discover' for details on missed commands.");
257}
258
259pub fn cmd_wrapped(args: &[String]) {
260    let period = if args.iter().any(|a| a == "--month") {
261        "month"
262    } else if args.iter().any(|a| a == "--all") {
263        "all"
264    } else {
265        "week"
266    };
267
268    eprintln!("[DEPRECATED] Use `lean-ctx gain --wrapped`.");
269    println!(
270        "{}",
271        crate::tools::ctx_gain::handle("wrapped", Some(period), None, None)
272    );
273}
274
275pub fn cmd_sessions(args: &[String]) {
276    use crate::core::session::SessionState;
277
278    let action = args.first().map_or("list", std::string::String::as_str);
279
280    match action {
281        "list" | "ls" => {
282            let sessions = SessionState::list_sessions();
283            if sessions.is_empty() {
284                println!("No sessions found.");
285                return;
286            }
287            println!("Sessions ({}):\n", sessions.len());
288            for s in sessions.iter().take(20) {
289                let task = s.task.as_deref().unwrap_or("(no task)");
290                let task_short: String = task.chars().take(50).collect();
291                let date = s.updated_at.format("%Y-%m-%d %H:%M");
292                println!(
293                    "  {} | v{:3} | {:5} calls | {:>8} tok | {} | {}",
294                    s.id,
295                    s.version,
296                    s.tool_calls,
297                    format_tokens_cli(s.tokens_saved),
298                    date,
299                    task_short
300                );
301            }
302            if sessions.len() > 20 {
303                println!("  ... +{} more", sessions.len() - 20);
304            }
305        }
306        "show" => {
307            let id = args.get(1);
308            let session = if let Some(id) = id {
309                SessionState::load_by_id(id)
310            } else {
311                SessionState::load_latest()
312            };
313            match session {
314                Some(s) => println!("{}", s.format_compact()),
315                None => println!("Session not found."),
316            }
317        }
318        "cleanup" => {
319            let days = args.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(7);
320            let removed = SessionState::cleanup_old_sessions(days);
321            println!("Cleaned up {removed} session(s) older than {days} days.");
322        }
323        _ => {
324            eprintln!("Usage: lean-ctx sessions [list|show [id]|cleanup [days]]");
325            std::process::exit(1);
326        }
327    }
328}