Skip to main content

lean_ctx/cli/
discover_cmd.rs

1use super::common::load_shell_history;
2
3pub fn cmd_discover(args: &[String]) {
4    let history = load_shell_history();
5    if history.is_empty() {
6        println!("No shell history found.");
7        return;
8    }
9
10    let result = crate::tools::ctx_discover::analyze_history(&history, 20);
11
12    if let Some(path) = card_target(args) {
13        match std::fs::write(
14            &path,
15            crate::tools::ctx_discover::render_before_card(&result),
16        ) {
17            Ok(()) => println!(
18                "Before-card written to {path}\n\
19                 Share it, or run `lean-ctx gain --wrapped` after a week for your real numbers."
20            ),
21            Err(e) => {
22                eprintln!("Failed to write {path}: {e}");
23                std::process::exit(1);
24            }
25        }
26        return;
27    }
28
29    println!("{}", crate::tools::ctx_discover::format_cli_output(&result));
30}
31
32/// Resolves the `--card[=<path>]` output path for the shareable "before" SVG, or `None`
33/// when not requested. A bare `--card` defaults to `lean-ctx-before.svg`.
34fn card_target(args: &[String]) -> Option<String> {
35    let mut requested = false;
36    let mut path: Option<String> = None;
37    for (i, a) in args.iter().enumerate() {
38        if let Some(v) = a.strip_prefix("--card=") {
39            requested = true;
40            path = Some(v.to_string());
41        } else if a == "--card" {
42            requested = true;
43            if let Some(next) = args.get(i + 1) {
44                if !next.starts_with('-') {
45                    path = Some(next.clone());
46                }
47            }
48        }
49    }
50    requested.then(|| path.unwrap_or_else(|| "lean-ctx-before.svg".to_string()))
51}
52
53/// Path of the marker recording that the first-run "aha" was already shown.
54fn first_run_marker() -> Option<std::path::PathBuf> {
55    dirs::home_dir().map(|h| h.join(".lean-ctx").join(".first_run_wow_done"))
56}
57
58/// Shows the discover "you're leaving X tokens on the table" moment exactly once, right
59/// after setup. Marker-guarded so re-running `setup` never repeats it. Stays silent (but
60/// still marks done) when there is too little history to be meaningful, so it never nags.
61/// Reads only local shell history and prints aggregate estimates — never command contents.
62pub fn show_first_run_wow() {
63    let Some(marker) = first_run_marker() else {
64        return;
65    };
66    if marker.exists() {
67        return;
68    }
69    // Mark immediately so any edge case never reshows on the next setup run.
70    if let Some(parent) = marker.parent() {
71        let _ = std::fs::create_dir_all(parent);
72    }
73    let _ = std::fs::write(&marker, "shown\n");
74
75    let history = load_shell_history();
76    if history.is_empty() {
77        return;
78    }
79    let result = crate::tools::ctx_discover::analyze_history(&history, 20);
80    let total_missed: u32 = result.missed_commands.iter().map(|m| m.count).sum();
81    if total_missed == 0 {
82        return;
83    }
84
85    let bold = "\x1b[1m";
86    let green = "\x1b[32m";
87    let yellow = "\x1b[33m";
88    let dim = "\x1b[2m";
89    let rst = "\x1b[0m";
90    let monthly = result.potential_usd * 30.0;
91    let saved = crate::core::wrapped::format_tokens(result.potential_tokens as u64);
92
93    println!();
94    println!("  {bold}Here's what lean-ctx just started saving you{rst}");
95    println!("  {dim}estimated from your shell history — nothing leaves your machine{rst}");
96    println!();
97    println!(
98        "  {green}~{saved} tokens{rst}{dim}/month{rst} across {total_missed} uncompressed commands {dim}(~${monthly:.0}/mo){rst}"
99    );
100    let top = result
101        .missed_commands
102        .iter()
103        .take(3)
104        .map(|m| format!("{} {}x", m.prefix, m.count))
105        .collect::<Vec<_>>()
106        .join("   ");
107    if !top.is_empty() {
108        println!("  {dim}top: {top}{rst}");
109    }
110    println!();
111    println!(
112        "  {yellow}Run {bold}lean-ctx gain --wrapped{rst}{yellow} after a week for your real numbers — and a shareable card.{rst}"
113    );
114    println!();
115}
116
117pub fn cmd_ghost(args: &[String]) {
118    let json = args.iter().any(|a| a == "--json");
119
120    let history = load_shell_history();
121    let discover = crate::tools::ctx_discover::analyze_history(&history, 20);
122
123    let session = crate::core::session::SessionState::load_latest();
124    let store = crate::core::stats::load();
125
126    let unoptimized_tokens = discover.potential_tokens;
127    let _unoptimized_usd = discover.potential_usd;
128
129    let redundant_reads = store.cep.total_cache_hits as usize;
130    let redundant_tokens = redundant_reads * 200;
131
132    let wasted_original = store
133        .cep
134        .total_tokens_original
135        .saturating_sub(store.cep.total_tokens_compressed) as usize;
136    let truncated_tokens = wasted_original / 3;
137
138    let total_ghost = unoptimized_tokens + redundant_tokens + truncated_tokens;
139    let total_usd =
140        total_ghost as f64 * crate::core::stats::DEFAULT_INPUT_PRICE_PER_M / 1_000_000.0;
141    let monthly_usd = total_usd * 30.0;
142
143    if json {
144        let obj = serde_json::json!({
145            "ghost_tokens": total_ghost,
146            "breakdown": {
147                "unoptimized_shells": unoptimized_tokens,
148                "redundant_reads": redundant_tokens,
149                "truncated_contexts": truncated_tokens,
150            },
151            "estimated_usd": total_usd,
152            "monthly_usd": monthly_usd,
153            "session_active": session.is_some(),
154            "history_commands": discover.total_commands,
155            "already_optimized": discover.already_optimized,
156        });
157        println!("{}", serde_json::to_string_pretty(&obj).unwrap_or_default());
158        return;
159    }
160
161    let bold = "\x1b[1m";
162    let green = "\x1b[32m";
163    let yellow = "\x1b[33m";
164    let red = "\x1b[31m";
165    let dim = "\x1b[2m";
166    let rst = "\x1b[0m";
167    let white = "\x1b[97m";
168
169    println!();
170    println!("  {bold}{white}lean-ctx ghost report{rst}");
171    println!("  {dim}{}{rst}", "=".repeat(40));
172    println!();
173
174    if total_ghost == 0 {
175        println!("  {green}No ghost tokens detected!{rst}");
176        println!(
177            "  {dim}All {} commands optimized.{rst}",
178            discover.total_commands
179        );
180        println!();
181        return;
182    }
183
184    let severity = if total_ghost > 10000 {
185        red
186    } else if total_ghost > 3000 {
187        yellow
188    } else {
189        green
190    };
191
192    println!(
193        "  {bold}Ghost Tokens found:{rst}     {severity}{total_ghost:>8}{rst} tokens {dim}(~${total_usd:.2}){rst}"
194    );
195    println!();
196
197    if unoptimized_tokens > 0 {
198        let missed_count: u32 = discover.missed_commands.iter().map(|m| m.count).sum();
199        println!(
200            "  {dim}  Unoptimized shells:{rst}  {white}{unoptimized_tokens:>8}{rst} {dim}({missed_count} cmds without lean-ctx){rst}"
201        );
202    }
203    if redundant_tokens > 0 {
204        println!(
205            "  {dim}  Redundant reads:{rst}     {white}{redundant_tokens:>8}{rst} {dim}({redundant_reads} cache hits = wasted re-reads){rst}"
206        );
207    }
208    if truncated_tokens > 0 {
209        println!(
210            "  {dim}  Oversized contexts:{rst}  {white}{truncated_tokens:>8}{rst} {dim}(uncompressed portion of tool results){rst}"
211        );
212    }
213
214    println!();
215    println!("  {bold}Monthly savings potential:{rst} {green}${monthly_usd:.2}{rst}");
216
217    if !discover.missed_commands.is_empty() {
218        println!();
219        println!("  {bold}Top unoptimized commands:{rst}");
220        for m in discover.missed_commands.iter().take(5) {
221            println!(
222                "    {dim}{:>4}x{rst}  {white}{:<12}{rst} {dim}{}{rst}",
223                m.count, m.prefix, m.description
224            );
225        }
226    }
227
228    println!();
229    if discover.already_optimized == 0 {
230        println!(
231            "  {yellow}Run '{bold}lean-ctx setup{rst}{yellow}' to eliminate ghost tokens.{rst}"
232        );
233    } else {
234        println!(
235            "  {dim}Already optimized: {}/{} commands{rst}",
236            discover.already_optimized, discover.total_commands
237        );
238    }
239    println!();
240}