lean_ctx/cli/
discover_cmd.rs1use 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
32fn 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
53fn first_run_marker() -> Option<std::path::PathBuf> {
55 dirs::home_dir().map(|h| h.join(".lean-ctx").join(".first_run_wow_done"))
56}
57
58pub fn show_first_run_wow() {
63 let Some(marker) = first_run_marker() else {
64 return;
65 };
66 if marker.exists() {
67 return;
68 }
69 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}