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    let monthly = result.potential_usd * 30.0;
93    let saved = crate::core::wrapped::format_tokens(result.potential_tokens as u64);
94
95    println!();
96    println!("  {bold}Here's what lean-ctx just started saving you{rst}");
97    println!("  {dim}estimated from your shell history — nothing leaves your machine{rst}");
98    println!();
99    println!(
100        "  {green}~{saved} tokens{rst}{dim}/month{rst} across {total_missed} uncompressed commands {dim}(~${monthly:.0}/mo){rst}"
101    );
102    let top = result
103        .missed_commands
104        .iter()
105        .take(3)
106        .map(|m| format!("{} {}x", m.prefix, m.count))
107        .collect::<Vec<_>>()
108        .join("   ");
109    if !top.is_empty() {
110        println!("  {dim}top: {top}{rst}");
111    }
112    println!();
113    println!(
114        "  {yellow}Run {bold}lean-ctx gain --wrapped{rst}{yellow} after a week for your real numbers — and a shareable card.{rst}"
115    );
116    println!();
117}
118
119pub fn cmd_ghost(args: &[String]) {
120    let json = args.iter().any(|a| a == "--json");
121
122    let history = load_shell_history();
123    let discover = crate::tools::ctx_discover::analyze_history(&history, 20);
124
125    let session = crate::core::session::SessionState::load_latest();
126    let store = crate::core::stats::load();
127
128    let unoptimized_tokens = discover.potential_tokens;
129    let _unoptimized_usd = discover.potential_usd;
130
131    let redundant_reads = store.cep.total_cache_hits as usize;
132    let redundant_tokens = redundant_reads * 200;
133
134    let wasted_original = store
135        .cep
136        .total_tokens_original
137        .saturating_sub(store.cep.total_tokens_compressed) as usize;
138    let truncated_tokens = wasted_original / 3;
139
140    let total_ghost = unoptimized_tokens + redundant_tokens + truncated_tokens;
141    let total_usd =
142        total_ghost as f64 * crate::core::stats::DEFAULT_INPUT_PRICE_PER_M / 1_000_000.0;
143    let monthly_usd = total_usd * 30.0;
144
145    if json {
146        let obj = serde_json::json!({
147            "ghost_tokens": total_ghost,
148            "breakdown": {
149                "unoptimized_shells": unoptimized_tokens,
150                "redundant_reads": redundant_tokens,
151                "truncated_contexts": truncated_tokens,
152            },
153            "estimated_usd": total_usd,
154            "monthly_usd": monthly_usd,
155            "session_active": session.is_some(),
156            "history_commands": discover.total_commands,
157            "already_optimized": discover.already_optimized,
158        });
159        println!("{}", serde_json::to_string_pretty(&obj).unwrap_or_default());
160        return;
161    }
162
163    let bold = "\x1b[1m";
164    let green = "\x1b[32m";
165    let yellow = "\x1b[33m";
166    let red = "\x1b[31m";
167    let dim = "\x1b[2m";
168    let rst = "\x1b[0m";
169    let white = "\x1b[97m";
170
171    println!();
172    println!("  {bold}{white}lean-ctx ghost report{rst}");
173    println!("  {dim}{}{rst}", "=".repeat(40));
174    println!();
175
176    if total_ghost == 0 {
177        println!("  {green}No ghost tokens detected!{rst}");
178        println!(
179            "  {dim}All {} commands optimized.{rst}",
180            discover.total_commands
181        );
182        println!();
183        return;
184    }
185
186    let severity = if total_ghost > 10000 {
187        red
188    } else if total_ghost > 3000 {
189        yellow
190    } else {
191        green
192    };
193
194    println!(
195        "  {bold}Ghost Tokens found:{rst}     {severity}{total_ghost:>8}{rst} tokens {dim}(~${total_usd:.2}){rst}"
196    );
197    println!();
198
199    if unoptimized_tokens > 0 {
200        let missed_count: u32 = discover.missed_commands.iter().map(|m| m.count).sum();
201        println!(
202            "  {dim}  Unoptimized shells:{rst}  {white}{unoptimized_tokens:>8}{rst} {dim}({missed_count} cmds without lean-ctx){rst}"
203        );
204    }
205    if redundant_tokens > 0 {
206        println!(
207            "  {dim}  Redundant reads:{rst}     {white}{redundant_tokens:>8}{rst} {dim}({redundant_reads} cache hits = wasted re-reads){rst}"
208        );
209    }
210    if truncated_tokens > 0 {
211        println!(
212            "  {dim}  Oversized contexts:{rst}  {white}{truncated_tokens:>8}{rst} {dim}(uncompressed portion of tool results){rst}"
213        );
214    }
215
216    println!();
217    println!("  {bold}Monthly savings potential:{rst} {green}${monthly_usd:.2}{rst}");
218
219    if !discover.missed_commands.is_empty() {
220        println!();
221        println!("  {bold}Top unoptimized commands:{rst}");
222        for m in discover.missed_commands.iter().take(5) {
223            println!(
224                "    {dim}{:>4}x{rst}  {white}{:<12}{rst} {dim}{}{rst}",
225                m.count, m.prefix, m.description
226            );
227        }
228    }
229
230    println!();
231    if discover.already_optimized == 0 {
232        println!(
233            "  {yellow}Run '{bold}lean-ctx setup{rst}{yellow}' to eliminate ghost tokens.{rst}"
234        );
235    } else {
236        println!(
237            "  {dim}Already optimized: {}/{} commands{rst}",
238            discover.already_optimized, discover.total_commands
239        );
240    }
241    println!();
242}