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                && !next.starts_with('-')
45            {
46                path = Some(next.clone());
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    crate::core::paths::cache_dir()
56        .ok()
57        .map(|d| d.join(".first_run_wow_done"))
58}
59
60/// Shows the discover "you're leaving X tokens on the table" moment exactly once, right
61/// after setup. Marker-guarded so re-running `setup` never repeats it. Stays silent (but
62/// still marks done) when there is too little history to be meaningful, so it never nags.
63/// Reads only local shell history and prints aggregate estimates — never command contents.
64pub fn show_first_run_wow() {
65    let Some(marker) = first_run_marker() else {
66        return;
67    };
68    if marker.exists() {
69        return;
70    }
71    // Mark immediately so any edge case never reshows on the next setup run.
72    if let Some(parent) = marker.parent() {
73        let _ = std::fs::create_dir_all(parent);
74    }
75    let _ = std::fs::write(&marker, "shown\n");
76
77    let history = load_shell_history();
78    if history.is_empty() {
79        return;
80    }
81    let result = crate::tools::ctx_discover::analyze_history(&history, 20);
82    let total_missed: u32 = result.missed_commands.iter().map(|m| m.count).sum();
83    if total_missed == 0 {
84        return;
85    }
86
87    let bold = "\x1b[1m";
88    let green = "\x1b[32m";
89    let yellow = "\x1b[33m";
90    let dim = "\x1b[2m";
91    let rst = "\x1b[0m";
92
93    let top = result
94        .missed_commands
95        .iter()
96        .take(3)
97        .map(|m| format!("{} {}x", m.prefix, m.count))
98        .collect::<Vec<_>>()
99        .join("   ");
100
101    println!();
102    println!("  {bold}Here's what lean-ctx is about to start saving you{rst}");
103    println!("  {dim}estimated from your shell history — nothing leaves your machine{rst}");
104    println!();
105    if result.has_measured_data {
106        // Returning user: project their own measured savings onto frequency.
107        let monthly = result.potential_usd * 30.0;
108        let saved = crate::core::wrapped::format_tokens(result.potential_tokens as u64);
109        println!(
110            "  {green}~{saved} tokens{rst}{dim}/month{rst} across {total_missed} uncompressed commands {dim}(~${monthly:.0}/mo, from your measured rate){rst}"
111        );
112    } else {
113        // Fresh install: no measurements yet — show frequency, never a fake $.
114        println!(
115            "  {green}{total_missed} commands{rst} {dim}that lean-ctx will now compress automatically{rst}"
116        );
117    }
118    if !top.is_empty() {
119        println!("  {dim}top: {top}{rst}");
120    }
121    println!();
122    println!(
123        "  {yellow}Run {bold}lean-ctx gain --wrapped{rst}{yellow} after a week for your real numbers — and a shareable card.{rst}"
124    );
125    println!();
126}
127
128pub fn cmd_ghost(args: &[String]) {
129    let json = args.iter().any(|a| a == "--json");
130
131    let history = load_shell_history();
132    let discover = crate::tools::ctx_discover::analyze_history(&history, 20);
133
134    let session = crate::core::session::SessionState::load_latest();
135    let store = crate::core::stats::load();
136
137    let unoptimized_tokens = discover.potential_tokens;
138    let _unoptimized_usd = discover.potential_usd;
139
140    // Redundant reads: every cache hit is a re-read lean-ctx served from cache
141    // instead of re-sending the file. Value it at the user's *measured* average
142    // read size (from the heatmap), never a guessed constant. No heatmap data
143    // yet => 0, so we never invent a figure.
144    let redundant_reads = store.cep.total_cache_hits as usize;
145    let avg_read_tokens = crate::core::heatmap::HeatMap::load()
146        .avg_original_tokens_per_access()
147        .unwrap_or(0) as usize;
148    let redundant_tokens = redundant_reads.saturating_mul(avg_read_tokens);
149
150    // NOTE: the former "oversized contexts" category was dropped — it was a
151    // fabricated wasted_original/3 heuristic applied to realized (not wasted)
152    // compression savings. The JSON keeps `truncated_contexts: 0` for schema
153    // stability; there is no honest data source to revive it.
154    let total_ghost = unoptimized_tokens + redundant_tokens;
155    let total_usd =
156        total_ghost as f64 * crate::core::stats::DEFAULT_INPUT_PRICE_PER_M / 1_000_000.0;
157    let monthly_usd = total_usd * 30.0;
158
159    if json {
160        let obj = serde_json::json!({
161            "ghost_tokens": total_ghost,
162            "breakdown": {
163                "unoptimized_shells": unoptimized_tokens,
164                "redundant_reads": redundant_tokens,
165                "truncated_contexts": 0,
166            },
167            "estimated_usd": total_usd,
168            "monthly_usd": monthly_usd,
169            "session_active": session.is_some(),
170            "history_commands": discover.total_commands,
171            "already_optimized": discover.already_optimized,
172        });
173        println!("{}", serde_json::to_string_pretty(&obj).unwrap_or_default());
174        return;
175    }
176
177    let bold = "\x1b[1m";
178    let green = "\x1b[32m";
179    let yellow = "\x1b[33m";
180    let red = "\x1b[31m";
181    let dim = "\x1b[2m";
182    let rst = "\x1b[0m";
183    let white = "\x1b[97m";
184
185    println!();
186    println!("  {bold}{white}lean-ctx ghost report{rst}");
187    println!("  {dim}{}{rst}", "=".repeat(40));
188    println!();
189
190    if total_ghost == 0 {
191        println!("  {green}No ghost tokens detected!{rst}");
192        println!(
193            "  {dim}All {} commands optimized.{rst}",
194            discover.total_commands
195        );
196        println!();
197        return;
198    }
199
200    let severity = if total_ghost > 10000 {
201        red
202    } else if total_ghost > 3000 {
203        yellow
204    } else {
205        green
206    };
207
208    println!(
209        "  {bold}Ghost Tokens found:{rst}     {severity}{total_ghost:>8}{rst} tokens {dim}(~${total_usd:.2}){rst}"
210    );
211    println!();
212
213    if unoptimized_tokens > 0 {
214        let missed_count: u32 = discover.missed_commands.iter().map(|m| m.count).sum();
215        println!(
216            "  {dim}  Unoptimized shells:{rst}  {white}{unoptimized_tokens:>8}{rst} {dim}({missed_count} cmds without lean-ctx){rst}"
217        );
218    }
219    if redundant_tokens > 0 {
220        println!(
221            "  {dim}  Redundant reads:{rst}     {white}{redundant_tokens:>8}{rst} {dim}({redundant_reads} cache hits = wasted re-reads){rst}"
222        );
223    }
224    println!();
225    println!("  {bold}Monthly savings potential:{rst} {green}${monthly_usd:.2}{rst}");
226
227    if !discover.missed_commands.is_empty() {
228        println!();
229        println!("  {bold}Top unoptimized commands:{rst}");
230        for m in discover.missed_commands.iter().take(5) {
231            println!(
232                "    {dim}{:>4}x{rst}  {white}{:<12}{rst} {dim}{}{rst}",
233                m.count, m.prefix, m.description
234            );
235        }
236    }
237
238    println!();
239    if discover.already_optimized == 0 {
240        println!(
241            "  {yellow}Run '{bold}lean-ctx onboard{rst}{yellow}' to eliminate ghost tokens.{rst}"
242        );
243    } else {
244        println!(
245            "  {dim}Already optimized: {}/{} commands{rst}",
246            discover.already_optimized, discover.total_commands
247        );
248    }
249    println!();
250}