1use crate::{
2 core, dashboard, doctor, heatmap, hook_handlers, mcp_stdio, report, setup, shell, status,
3 token_report, tools, tui, uninstall,
4};
5use anyhow::Result;
6
7pub fn run() {
8 let args: Vec<String> = std::env::args().collect();
9
10 if args.len() > 1 {
11 let rest = args[2..].to_vec();
12
13 match args[1].as_str() {
14 "-c" | "exec" => {
15 let raw = rest.first().is_some_and(|a| a == "--raw");
16 let cmd_args = if raw { &args[3..] } else { &args[2..] };
17 let command = if cmd_args.len() == 1 {
18 cmd_args[0].clone()
19 } else {
20 shell::join_command(cmd_args)
21 };
22 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
23 || std::env::var("LEAN_CTX_DISABLED").is_ok()
24 {
25 passthrough(&command);
26 }
27 if raw {
28 std::env::set_var("LEAN_CTX_RAW", "1");
29 } else {
30 std::env::set_var("LEAN_CTX_COMPRESS", "1");
31 }
32 let code = shell::exec(&command);
33 core::stats::flush();
34 std::process::exit(code);
35 }
36 "-t" | "--track" => {
37 let cmd_args = &args[2..];
38 let code = if cmd_args.len() > 1 {
39 shell::exec_argv(cmd_args)
40 } else {
41 let command = cmd_args[0].clone();
42 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
43 || std::env::var("LEAN_CTX_DISABLED").is_ok()
44 {
45 passthrough(&command);
46 }
47 shell::exec(&command)
48 };
49 core::stats::flush();
50 std::process::exit(code);
51 }
52 "shell" | "--shell" => {
53 shell::interactive();
54 return;
55 }
56 "gain" => {
57 if rest.iter().any(|a| a == "--reset") {
58 core::stats::reset_all();
59 println!("Stats reset. All token savings data cleared.");
60 return;
61 }
62 if rest.iter().any(|a| a == "--live" || a == "--watch") {
63 core::stats::gain_live();
64 return;
65 }
66 let model = rest.iter().enumerate().find_map(|(i, a)| {
67 if let Some(v) = a.strip_prefix("--model=") {
68 return Some(v.to_string());
69 }
70 if a == "--model" {
71 return rest.get(i + 1).cloned();
72 }
73 None
74 });
75 let period = rest
76 .iter()
77 .enumerate()
78 .find_map(|(i, a)| {
79 if let Some(v) = a.strip_prefix("--period=") {
80 return Some(v.to_string());
81 }
82 if a == "--period" {
83 return rest.get(i + 1).cloned();
84 }
85 None
86 })
87 .unwrap_or_else(|| "all".to_string());
88 let limit = rest
89 .iter()
90 .enumerate()
91 .find_map(|(i, a)| {
92 if let Some(v) = a.strip_prefix("--limit=") {
93 return v.parse::<usize>().ok();
94 }
95 if a == "--limit" {
96 return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
97 }
98 None
99 })
100 .unwrap_or(10);
101
102 if rest.iter().any(|a| a == "--graph") {
103 println!("{}", core::stats::format_gain_graph());
104 } else if rest.iter().any(|a| a == "--daily") {
105 println!("{}", core::stats::format_gain_daily());
106 } else if rest.iter().any(|a| a == "--json") {
107 println!(
108 "{}",
109 tools::ctx_gain::handle(
110 "json",
111 Some(&period),
112 model.as_deref(),
113 Some(limit)
114 )
115 );
116 } else if rest.iter().any(|a| a == "--score") {
117 println!(
118 "{}",
119 tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
120 );
121 } else if rest.iter().any(|a| a == "--cost") {
122 println!(
123 "{}",
124 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
125 );
126 } else if rest.iter().any(|a| a == "--tasks") {
127 println!(
128 "{}",
129 tools::ctx_gain::handle("tasks", None, None, Some(limit))
130 );
131 } else if rest.iter().any(|a| a == "--agents") {
132 println!(
133 "{}",
134 tools::ctx_gain::handle("agents", None, None, Some(limit))
135 );
136 } else if rest.iter().any(|a| a == "--heatmap") {
137 println!(
138 "{}",
139 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
140 );
141 } else if rest.iter().any(|a| a == "--wrapped") {
142 println!(
143 "{}",
144 tools::ctx_gain::handle(
145 "wrapped",
146 Some(&period),
147 model.as_deref(),
148 Some(limit)
149 )
150 );
151 } else if rest.iter().any(|a| a == "--pipeline") {
152 let stats_path = dirs::home_dir()
153 .unwrap_or_default()
154 .join(".lean-ctx")
155 .join("pipeline_stats.json");
156 if let Ok(data) = std::fs::read_to_string(&stats_path) {
157 if let Ok(stats) =
158 serde_json::from_str::<core::pipeline::PipelineStats>(&data)
159 {
160 println!("{}", stats.format_summary());
161 } else {
162 println!("No pipeline stats available yet (corrupt data).");
163 }
164 } else {
165 println!(
166 "No pipeline stats available yet. Use MCP tools to generate data."
167 );
168 }
169 } else if rest.iter().any(|a| a == "--deep") {
170 println!(
171 "{}\n{}\n{}\n{}\n{}",
172 tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
173 tools::ctx_gain::handle("tasks", None, None, Some(limit)),
174 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
175 tools::ctx_gain::handle("agents", None, None, Some(limit)),
176 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
177 );
178 } else {
179 println!("{}", core::stats::format_gain());
180 }
181 return;
182 }
183 "token-report" | "report-tokens" => {
184 let code = token_report::run_cli(&rest);
185 if code != 0 {
186 std::process::exit(code);
187 }
188 return;
189 }
190 "cep" => {
191 println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
192 return;
193 }
194 "dashboard" => {
195 let port = rest
196 .iter()
197 .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
198 .and_then(|p| p.parse().ok());
199 let host = rest
200 .iter()
201 .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
202 .map(String::from);
203 let project = rest
204 .iter()
205 .find_map(|p| p.strip_prefix("--project="))
206 .map(String::from);
207 if let Some(ref p) = project {
208 std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
209 }
210 run_async(dashboard::start(port, host));
211 return;
212 }
213 "serve" => {
214 #[cfg(feature = "http-server")]
215 {
216 let mut cfg = crate::http_server::HttpServerConfig::default();
217 let mut i = 0;
218 while i < rest.len() {
219 match rest[i].as_str() {
220 "--host" | "-H" => {
221 i += 1;
222 if i < rest.len() {
223 cfg.host.clone_from(&rest[i]);
224 }
225 }
226 arg if arg.starts_with("--host=") => {
227 cfg.host = arg["--host=".len()..].to_string();
228 }
229 "--port" | "-p" => {
230 i += 1;
231 if i < rest.len() {
232 if let Ok(p) = rest[i].parse::<u16>() {
233 cfg.port = p;
234 }
235 }
236 }
237 arg if arg.starts_with("--port=") => {
238 if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
239 cfg.port = p;
240 }
241 }
242 "--project-root" => {
243 i += 1;
244 if i < rest.len() {
245 cfg.project_root = std::path::PathBuf::from(&rest[i]);
246 }
247 }
248 arg if arg.starts_with("--project-root=") => {
249 cfg.project_root =
250 std::path::PathBuf::from(&arg["--project-root=".len()..]);
251 }
252 "--auth-token" => {
253 i += 1;
254 if i < rest.len() {
255 cfg.auth_token = Some(rest[i].clone());
256 }
257 }
258 arg if arg.starts_with("--auth-token=") => {
259 cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
260 }
261 "--stateful" => cfg.stateful_mode = true,
262 "--stateless" => cfg.stateful_mode = false,
263 "--json" => cfg.json_response = true,
264 "--sse" => cfg.json_response = false,
265 "--disable-host-check" => cfg.disable_host_check = true,
266 "--allowed-host" => {
267 i += 1;
268 if i < rest.len() {
269 cfg.allowed_hosts.push(rest[i].clone());
270 }
271 }
272 arg if arg.starts_with("--allowed-host=") => {
273 cfg.allowed_hosts
274 .push(arg["--allowed-host=".len()..].to_string());
275 }
276 "--max-body-bytes" => {
277 i += 1;
278 if i < rest.len() {
279 if let Ok(n) = rest[i].parse::<usize>() {
280 cfg.max_body_bytes = n;
281 }
282 }
283 }
284 arg if arg.starts_with("--max-body-bytes=") => {
285 if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
286 cfg.max_body_bytes = n;
287 }
288 }
289 "--max-concurrency" => {
290 i += 1;
291 if i < rest.len() {
292 if let Ok(n) = rest[i].parse::<usize>() {
293 cfg.max_concurrency = n;
294 }
295 }
296 }
297 arg if arg.starts_with("--max-concurrency=") => {
298 if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
299 cfg.max_concurrency = n;
300 }
301 }
302 "--max-rps" => {
303 i += 1;
304 if i < rest.len() {
305 if let Ok(n) = rest[i].parse::<u32>() {
306 cfg.max_rps = n;
307 }
308 }
309 }
310 arg if arg.starts_with("--max-rps=") => {
311 if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
312 cfg.max_rps = n;
313 }
314 }
315 "--rate-burst" => {
316 i += 1;
317 if i < rest.len() {
318 if let Ok(n) = rest[i].parse::<u32>() {
319 cfg.rate_burst = n;
320 }
321 }
322 }
323 arg if arg.starts_with("--rate-burst=") => {
324 if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
325 cfg.rate_burst = n;
326 }
327 }
328 "--request-timeout-ms" => {
329 i += 1;
330 if i < rest.len() {
331 if let Ok(n) = rest[i].parse::<u64>() {
332 cfg.request_timeout_ms = n;
333 }
334 }
335 }
336 arg if arg.starts_with("--request-timeout-ms=") => {
337 if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
338 cfg.request_timeout_ms = n;
339 }
340 }
341 "--help" | "-h" => {
342 eprintln!(
343 "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR]\\n\\
344 \\n\\
345 Options:\\n\\
346 --host, -H Bind host (default: 127.0.0.1)\\n\\
347 --port, -p Bind port (default: 8080)\\n\\
348 --project-root Resolve relative paths against this root (default: cwd)\\n\\
349 --auth-token Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
350 --stateful/--stateless Streamable HTTP session mode (default: stateless)\\n\\
351 --json/--sse Response framing in stateless mode (default: json)\\n\\
352 --max-body-bytes Max request body size in bytes (default: 2097152)\\n\\
353 --max-concurrency Max concurrent requests (default: 32)\\n\\
354 --max-rps Max requests/sec (global, default: 50)\\n\\
355 --rate-burst Rate limiter burst (global, default: 100)\\n\\
356 --request-timeout-ms REST tool-call timeout (default: 30000)\\n\\
357 --allowed-host Add allowed Host header (repeatable)\\n\\
358 --disable-host-check Disable Host header validation (unsafe)"
359 );
360 return;
361 }
362 _ => {}
363 }
364 i += 1;
365 }
366
367 if cfg.auth_token.is_none() {
368 if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
369 if !v.trim().is_empty() {
370 cfg.auth_token = Some(v);
371 }
372 }
373 }
374
375 if let Err(e) = run_async(crate::http_server::serve(cfg)) {
376 tracing::error!("HTTP server error: {e}");
377 std::process::exit(1);
378 }
379 return;
380 }
381 #[cfg(not(feature = "http-server"))]
382 {
383 eprintln!("lean-ctx serve is not available in this build");
384 std::process::exit(1);
385 }
386 }
387 "watch" => {
388 if let Err(e) = tui::run() {
389 tracing::error!("TUI error: {e}");
390 std::process::exit(1);
391 }
392 return;
393 }
394 "proxy" => {
395 #[cfg(feature = "http-server")]
396 {
397 let sub = rest.first().map_or("help", std::string::String::as_str);
398 match sub {
399 "start" => {
400 let port: u16 = rest
401 .iter()
402 .find_map(|p| {
403 p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
404 })
405 .and_then(|p| p.parse().ok())
406 .unwrap_or(4444);
407 let autostart = rest.iter().any(|a| a == "--autostart");
408 if autostart {
409 crate::proxy_autostart::install(port, false);
410 return;
411 }
412 if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
413 tracing::error!("Proxy error: {e}");
414 std::process::exit(1);
415 }
416 }
417 "stop" => {
418 match ureq::get(&format!(
419 "http://127.0.0.1:{}/health",
420 rest.iter()
421 .find_map(|p| p.strip_prefix("--port="))
422 .and_then(|p| p.parse::<u16>().ok())
423 .unwrap_or(4444)
424 ))
425 .call()
426 {
427 Ok(_) => {
428 println!("Proxy is running. Use Ctrl+C or kill the process.");
429 }
430 Err(_) => {
431 println!("No proxy running on that port.");
432 }
433 }
434 }
435 "status" => {
436 let port: u16 = rest
437 .iter()
438 .find_map(|p| p.strip_prefix("--port="))
439 .and_then(|p| p.parse().ok())
440 .unwrap_or(4444);
441 if let Ok(resp) =
442 ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
443 {
444 let body = resp.into_body().read_to_string().unwrap_or_default();
445 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
446 println!("lean-ctx proxy status:");
447 println!(" Requests: {}", v["requests_total"]);
448 println!(" Compressed: {}", v["requests_compressed"]);
449 println!(" Tokens saved: {}", v["tokens_saved"]);
450 println!(
451 " Compression: {}%",
452 v["compression_ratio_pct"].as_str().unwrap_or("0.0")
453 );
454 } else {
455 println!("{body}");
456 }
457 } else {
458 println!("No proxy running on port {port}.");
459 println!("Start with: lean-ctx proxy start");
460 }
461 }
462 _ => {
463 println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
464 }
465 }
466 return;
467 }
468 #[cfg(not(feature = "http-server"))]
469 {
470 eprintln!("lean-ctx proxy is not available in this build");
471 std::process::exit(1);
472 }
473 }
474 "init" => {
475 super::cmd_init(&rest);
476 return;
477 }
478 "setup" => {
479 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
480 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
481 let fix = rest.iter().any(|a| a == "--fix");
482 let json = rest.iter().any(|a| a == "--json");
483
484 if non_interactive || fix || json || yes {
485 let opts = setup::SetupOptions {
486 non_interactive,
487 yes,
488 fix,
489 json,
490 };
491 match setup::run_setup_with_options(opts) {
492 Ok(report) => {
493 if json {
494 println!(
495 "{}",
496 serde_json::to_string_pretty(&report)
497 .unwrap_or_else(|_| "{}".to_string())
498 );
499 }
500 if !report.success {
501 std::process::exit(1);
502 }
503 }
504 Err(e) => {
505 eprintln!("{e}");
506 std::process::exit(1);
507 }
508 }
509 } else {
510 setup::run_setup();
511 }
512 return;
513 }
514 "bootstrap" => {
515 let json = rest.iter().any(|a| a == "--json");
516 let opts = setup::SetupOptions {
517 non_interactive: true,
518 yes: true,
519 fix: true,
520 json,
521 };
522 match setup::run_setup_with_options(opts) {
523 Ok(report) => {
524 if json {
525 println!(
526 "{}",
527 serde_json::to_string_pretty(&report)
528 .unwrap_or_else(|_| "{}".to_string())
529 );
530 }
531 if !report.success {
532 std::process::exit(1);
533 }
534 }
535 Err(e) => {
536 eprintln!("{e}");
537 std::process::exit(1);
538 }
539 }
540 return;
541 }
542 "status" => {
543 let code = status::run_cli(&rest);
544 if code != 0 {
545 std::process::exit(code);
546 }
547 return;
548 }
549 "read" => {
550 super::cmd_read(&rest);
551 return;
552 }
553 "diff" => {
554 super::cmd_diff(&rest);
555 return;
556 }
557 "grep" => {
558 super::cmd_grep(&rest);
559 return;
560 }
561 "find" => {
562 super::cmd_find(&rest);
563 return;
564 }
565 "ls" => {
566 super::cmd_ls(&rest);
567 return;
568 }
569 "deps" => {
570 super::cmd_deps(&rest);
571 return;
572 }
573 "discover" => {
574 super::cmd_discover(&rest);
575 return;
576 }
577 "ghost" => {
578 super::cmd_ghost(&rest);
579 return;
580 }
581 "filter" => {
582 super::cmd_filter(&rest);
583 return;
584 }
585 "heatmap" => {
586 heatmap::cmd_heatmap(&rest);
587 return;
588 }
589 "graph" => {
590 let mut action = "build";
591 let mut path_arg: Option<&str> = None;
592 for arg in &rest {
593 if arg == "build" {
594 action = "build";
595 } else {
596 path_arg = Some(arg.as_str());
597 }
598 }
599 let root = path_arg
600 .map(String::from)
601 .or_else(|| {
602 std::env::current_dir()
603 .ok()
604 .map(|p| p.to_string_lossy().to_string())
605 })
606 .unwrap_or_else(|| ".".to_string());
607 match action {
608 "build" => {
609 let index = core::graph_index::load_or_build(&root);
610 println!(
611 "Graph built: {} files, {} edges",
612 index.files.len(),
613 index.edges.len()
614 );
615 }
616 _ => {
617 eprintln!("Usage: lean-ctx graph [build] [path]");
618 }
619 }
620 return;
621 }
622 "session" => {
623 super::cmd_session();
624 return;
625 }
626 "wrapped" => {
627 super::cmd_wrapped(&rest);
628 return;
629 }
630 "sessions" => {
631 super::cmd_sessions(&rest);
632 return;
633 }
634 "benchmark" => {
635 super::cmd_benchmark(&rest);
636 return;
637 }
638 "profile" => {
639 super::cmd_profile(&rest);
640 return;
641 }
642 "config" => {
643 super::cmd_config(&rest);
644 return;
645 }
646 "stats" => {
647 super::cmd_stats(&rest);
648 return;
649 }
650 "cache" => {
651 super::cmd_cache(&rest);
652 return;
653 }
654 "theme" => {
655 super::cmd_theme(&rest);
656 return;
657 }
658 "tee" => {
659 super::cmd_tee(&rest);
660 return;
661 }
662 "terse" => {
663 super::cmd_terse(&rest);
664 return;
665 }
666 "slow-log" => {
667 super::cmd_slow_log(&rest);
668 return;
669 }
670 "update" | "--self-update" => {
671 core::updater::run(&rest);
672 return;
673 }
674 "doctor" => {
675 let code = doctor::run_cli(&rest);
676 if code != 0 {
677 std::process::exit(code);
678 }
679 return;
680 }
681 "gotchas" | "bugs" => {
682 super::cloud::cmd_gotchas(&rest);
683 return;
684 }
685 "buddy" | "pet" => {
686 super::cloud::cmd_buddy(&rest);
687 return;
688 }
689 "hook" => {
690 let action = rest.first().map_or("help", std::string::String::as_str);
691 match action {
692 "rewrite" => hook_handlers::handle_rewrite(),
693 "redirect" => hook_handlers::handle_redirect(),
694 "copilot" => hook_handlers::handle_copilot(),
695 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
696 "codex-session-start" => hook_handlers::handle_codex_session_start(),
697 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
698 _ => {
699 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
700 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
701 std::process::exit(1);
702 }
703 }
704 return;
705 }
706 "report-issue" | "report" => {
707 report::run(&rest);
708 return;
709 }
710 "uninstall" => {
711 let dry_run = rest.iter().any(|a| a == "--dry-run");
712 uninstall::run(dry_run);
713 return;
714 }
715 "bypass" => {
716 if rest.is_empty() {
717 eprintln!("Usage: lean-ctx bypass \"command\"");
718 eprintln!("Runs the command with zero compression (raw passthrough).");
719 std::process::exit(1);
720 }
721 let command = if rest.len() == 1 {
722 rest[0].clone()
723 } else {
724 shell::join_command(&args[2..])
725 };
726 std::env::set_var("LEAN_CTX_RAW", "1");
727 let code = shell::exec(&command);
728 std::process::exit(code);
729 }
730 "safety-levels" | "safety" => {
731 println!("{}", core::compression_safety::format_safety_table());
732 return;
733 }
734 "cheat" | "cheatsheet" | "cheat-sheet" => {
735 super::cmd_cheatsheet();
736 return;
737 }
738 "login" => {
739 super::cloud::cmd_login(&rest);
740 return;
741 }
742 "register" => {
743 super::cloud::cmd_register(&rest);
744 return;
745 }
746 "forgot-password" => {
747 super::cloud::cmd_forgot_password(&rest);
748 return;
749 }
750 "sync" => {
751 super::cloud::cmd_sync();
752 return;
753 }
754 "contribute" => {
755 super::cloud::cmd_contribute();
756 return;
757 }
758 "cloud" => {
759 super::cloud::cmd_cloud(&rest);
760 return;
761 }
762 "upgrade" => {
763 super::cloud::cmd_upgrade();
764 return;
765 }
766 "--version" | "-V" => {
767 println!("{}", core::integrity::origin_line());
768 return;
769 }
770 "--help" | "-h" => {
771 print_help();
772 return;
773 }
774 "mcp" => {}
775 _ => {
776 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
777 print_help();
778 std::process::exit(1);
779 }
780 }
781 }
782
783 if let Err(e) = run_mcp_server() {
784 tracing::error!("lean-ctx: {e}");
785 std::process::exit(1);
786 }
787}
788
789fn passthrough(command: &str) -> ! {
790 let (shell, flag) = shell::shell_and_flag();
791 let status = std::process::Command::new(&shell)
792 .arg(&flag)
793 .arg(command)
794 .env("LEAN_CTX_ACTIVE", "1")
795 .status()
796 .map_or(127, |s| s.code().unwrap_or(1));
797 std::process::exit(status);
798}
799
800fn run_async<F: std::future::Future>(future: F) -> F::Output {
801 tokio::runtime::Runtime::new()
802 .expect("failed to create async runtime")
803 .block_on(future)
804}
805
806fn run_mcp_server() -> Result<()> {
807 use rmcp::ServiceExt;
808 use tracing_subscriber::EnvFilter;
809
810 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
811
812 let rt = tokio::runtime::Runtime::new()?;
813 rt.block_on(async {
814 tracing_subscriber::fmt()
815 .with_env_filter(EnvFilter::from_default_env())
816 .with_writer(std::io::stderr)
817 .init();
818
819 tracing::info!(
820 "lean-ctx v{} MCP server starting",
821 env!("CARGO_PKG_VERSION")
822 );
823
824 let server = tools::create_server();
825 let transport =
826 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
827 let service = match server.serve(transport).await {
828 Ok(s) => s,
829 Err(e) => {
830 let msg = e.to_string();
831 if msg.contains("expect initialized")
832 || msg.contains("context canceled")
833 || msg.contains("broken pipe")
834 {
835 tracing::debug!("Client disconnected before init: {msg}");
836 return Ok(());
837 }
838 return Err(e.into());
839 }
840 };
841 match service.waiting().await {
842 Ok(reason) => {
843 tracing::info!("MCP server stopped: {reason:?}");
844 }
845 Err(e) => {
846 let msg = e.to_string();
847 if msg.contains("broken pipe")
848 || msg.contains("connection reset")
849 || msg.contains("context canceled")
850 {
851 tracing::info!("MCP server: transport closed ({msg})");
852 } else {
853 tracing::error!("MCP server error: {msg}");
854 }
855 }
856 }
857
858 core::stats::flush();
859 core::mode_predictor::ModePredictor::flush();
860 core::feedback::FeedbackStore::flush();
861
862 Ok(())
863 })
864}
865
866fn print_help() {
867 println!(
868 "lean-ctx {version} — Context Runtime for AI Agents
869
87090+ compression patterns | 48 MCP tools | Context Continuity Protocol
871
872USAGE:
873 lean-ctx Start MCP server (stdio)
874 lean-ctx serve Start MCP server (Streamable HTTP)
875 lean-ctx -t \"command\" Track command (full output + stats, no compression)
876 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
877 lean-ctx -c --raw \"command\" Execute without compression (full output)
878 lean-ctx exec \"command\" Same as -c
879 lean-ctx shell Interactive shell with compression
880
881COMMANDS:
882 gain Visual dashboard (colors, bars, sparklines, USD)
883 gain --live Live mode: auto-refreshes every 1s in-place
884 gain --graph 30-day savings chart
885 gain --daily Bordered day-by-day table with USD
886 gain --json Raw JSON export of all stats
887 token-report [--json] Token + memory report (project + session + CEP)
888 cep CEP impact report (score trends, cache, modes)
889 watch Live TUI dashboard (real-time event stream)
890 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
891 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
892 proxy start [--port=4444] API proxy: compress tool_results before LLM API
893 proxy status Show proxy statistics
894 cache [list|clear|stats] Show/manage file read cache
895 wrapped [--week|--month|--all] Savings report card (shareable)
896 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
897 benchmark run [path] [--json] Run real benchmark on project files
898 benchmark report [path] Generate shareable Markdown report
899 cheatsheet Command cheat sheet & workflow quick reference
900 setup One-command setup: shell + editor + verify
901 bootstrap Non-interactive setup + fix (zero-config)
902 status [--json] Show setup + MCP + rules status
903 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
904 init --agent <name> Configure MCP for specific editor/agent
905 read <file> [-m mode] Read file with compression
906 diff <file1> <file2> Compressed file diff
907 grep <pattern> [path] Search with compressed output
908 find <pattern> [path] Find files with compressed output
909 ls [path] Directory listing with compression
910 deps [path] Show project dependencies
911 discover Find uncompressed commands in shell history
912 ghost [--json] Ghost Token report: find hidden token waste
913 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
914 session Show adoption statistics
915 config Show/edit configuration (~/.lean-ctx/config.toml)
916 profile [list|show|diff|create|set] Manage context profiles
917 theme [list|set|export|import] Customize terminal colors and themes
918 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
919 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
920 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
921 update [--check] Self-update lean-ctx binary from GitHub Releases
922 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
923 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
924 doctor [--fix] [--json] Run diagnostics (and optionally repair)
925 uninstall Remove shell hook, MCP configs, and data directory
926
927SHELL HOOK PATTERNS (90+):
928 git status, log, diff, add, commit, push, pull, fetch, clone,
929 branch, checkout, switch, merge, stash, tag, reset, remote
930 docker build, ps, images, logs, compose, exec, network
931 npm/pnpm install, test, run, list, outdated, audit
932 cargo build, test, check, clippy
933 gh pr list/view/create, issue list/view, run list/view
934 kubectl get pods/services/deployments, logs, describe, apply
935 python pip install/list/outdated, ruff check/format, poetry, uv
936 linters eslint, biome, prettier, golangci-lint
937 builds tsc, next build, vite build
938 ruby rubocop, bundle install/update, rake test, rails test
939 tests jest, vitest, pytest, go test, playwright, rspec, minitest
940 iac terraform, make, maven, gradle, dotnet, flutter, dart
941 utils curl, grep/rg, find, ls, wget, env
942 data JSON schema extraction, log deduplication
943
944READ MODES:
945 auto Auto-select optimal mode (default)
946 full Full content (cached re-reads = 13 tokens)
947 map Dependency graph + API signatures
948 signatures tree-sitter AST extraction (18 languages)
949 task Task-relevant filtering (requires ctx_session task)
950 reference One-line reference stub (cheap cache key)
951 aggressive Syntax-stripped content
952 entropy Shannon entropy filtered
953 diff Changed lines only
954 lines:N-M Specific line ranges (e.g. lines:10-50,80)
955
956ENVIRONMENT:
957 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
958 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
959 LEAN_CTX_RAW=1 Same as --raw for current command
960 LEAN_CTX_AUTONOMY=false Disable autonomous features
961 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
962
963OPTIONS:
964 --version, -V Show version
965 --help, -h Show this help
966
967EXAMPLES:
968 lean-ctx -c \"git status\" Compressed git output
969 lean-ctx -c \"kubectl get pods\" Compressed k8s output
970 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
971 lean-ctx gain Visual terminal dashboard
972 lean-ctx gain --live Live auto-updating terminal dashboard
973 lean-ctx gain --graph 30-day savings chart
974 lean-ctx gain --daily Day-by-day breakdown with USD
975 lean-ctx token-report --json Machine-readable token + memory report
976 lean-ctx dashboard Open web dashboard at localhost:3333
977 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
978 lean-ctx wrapped Weekly savings report card
979 lean-ctx wrapped --month Monthly savings report card
980 lean-ctx sessions list List all CCP sessions
981 lean-ctx sessions show Show latest session state
982 lean-ctx discover Find missed savings in shell history
983 lean-ctx setup One-command setup (shell + editors + verify)
984 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
985 lean-ctx bootstrap --json Machine-readable bootstrap report
986 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
987 lean-ctx-on Enable shell aliases in track mode (full output + stats)
988 lean-ctx-off Disable all shell aliases
989 lean-ctx-mode track Track mode: full output, stats recorded (default)
990 lean-ctx-mode compress Compress mode: all output compressed (power users)
991 lean-ctx-mode off Same as lean-ctx-off
992 lean-ctx-status Show whether compression is active
993 lean-ctx init --agent pi Install Pi Coding Agent extension
994 lean-ctx doctor Check PATH, config, MCP, and dashboard port
995 lean-ctx doctor --fix --json Repair + machine-readable report
996 lean-ctx status --json Machine-readable current status
997 lean-ctx read src/main.rs -m map
998 lean-ctx grep \"pub fn\" src/
999 lean-ctx deps .
1000
1001CLOUD:
1002 cloud status Show cloud connection status
1003 login <email> Log into existing LeanCTX Cloud account
1004 register <email> Create a new LeanCTX Cloud account
1005 forgot-password <email> Send password reset email
1006 sync Upload local stats to cloud dashboard
1007 contribute Share anonymized compression data
1008
1009TROUBLESHOOTING:
1010 Commands broken? lean-ctx-off (fixes current session)
1011 Permanent fix? lean-ctx uninstall (removes all hooks)
1012 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1013 Binary missing? Aliases auto-fallback to original commands (safe)
1014 Preview init? lean-ctx init --global --dry-run
1015
1016WEBSITE: https://leanctx.com
1017GITHUB: https://github.com/yvgude/lean-ctx
1018",
1019 version = env!("CARGO_PKG_VERSION"),
1020 );
1021}