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 "config" => {
639 super::cmd_config(&rest);
640 return;
641 }
642 "stats" => {
643 super::cmd_stats(&rest);
644 return;
645 }
646 "cache" => {
647 super::cmd_cache(&rest);
648 return;
649 }
650 "theme" => {
651 super::cmd_theme(&rest);
652 return;
653 }
654 "tee" => {
655 super::cmd_tee(&rest);
656 return;
657 }
658 "terse" => {
659 super::cmd_terse(&rest);
660 return;
661 }
662 "slow-log" => {
663 super::cmd_slow_log(&rest);
664 return;
665 }
666 "update" | "--self-update" => {
667 core::updater::run(&rest);
668 return;
669 }
670 "doctor" => {
671 let code = doctor::run_cli(&rest);
672 if code != 0 {
673 std::process::exit(code);
674 }
675 return;
676 }
677 "gotchas" | "bugs" => {
678 super::cloud::cmd_gotchas(&rest);
679 return;
680 }
681 "buddy" | "pet" => {
682 super::cloud::cmd_buddy(&rest);
683 return;
684 }
685 "hook" => {
686 let action = rest.first().map_or("help", std::string::String::as_str);
687 match action {
688 "rewrite" => hook_handlers::handle_rewrite(),
689 "redirect" => hook_handlers::handle_redirect(),
690 "copilot" => hook_handlers::handle_copilot(),
691 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
692 "codex-session-start" => hook_handlers::handle_codex_session_start(),
693 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
694 _ => {
695 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
696 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
697 std::process::exit(1);
698 }
699 }
700 return;
701 }
702 "report-issue" | "report" => {
703 report::run(&rest);
704 return;
705 }
706 "uninstall" => {
707 let dry_run = rest.iter().any(|a| a == "--dry-run");
708 uninstall::run(dry_run);
709 return;
710 }
711 "bypass" => {
712 if rest.is_empty() {
713 eprintln!("Usage: lean-ctx bypass \"command\"");
714 eprintln!("Runs the command with zero compression (raw passthrough).");
715 std::process::exit(1);
716 }
717 let command = if rest.len() == 1 {
718 rest[0].clone()
719 } else {
720 shell::join_command(&args[2..])
721 };
722 std::env::set_var("LEAN_CTX_RAW", "1");
723 let code = shell::exec(&command);
724 std::process::exit(code);
725 }
726 "safety-levels" | "safety" => {
727 println!("{}", core::compression_safety::format_safety_table());
728 return;
729 }
730 "cheat" | "cheatsheet" | "cheat-sheet" => {
731 super::cmd_cheatsheet();
732 return;
733 }
734 "login" => {
735 super::cloud::cmd_login(&rest);
736 return;
737 }
738 "register" => {
739 super::cloud::cmd_register(&rest);
740 return;
741 }
742 "forgot-password" => {
743 super::cloud::cmd_forgot_password(&rest);
744 return;
745 }
746 "sync" => {
747 super::cloud::cmd_sync();
748 return;
749 }
750 "contribute" => {
751 super::cloud::cmd_contribute();
752 return;
753 }
754 "cloud" => {
755 super::cloud::cmd_cloud(&rest);
756 return;
757 }
758 "upgrade" => {
759 super::cloud::cmd_upgrade();
760 return;
761 }
762 "--version" | "-V" => {
763 println!("{}", core::integrity::origin_line());
764 return;
765 }
766 "--help" | "-h" => {
767 print_help();
768 return;
769 }
770 "mcp" => {}
771 _ => {
772 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
773 print_help();
774 std::process::exit(1);
775 }
776 }
777 }
778
779 if let Err(e) = run_mcp_server() {
780 tracing::error!("lean-ctx: {e}");
781 std::process::exit(1);
782 }
783}
784
785fn passthrough(command: &str) -> ! {
786 let (shell, flag) = shell::shell_and_flag();
787 let status = std::process::Command::new(&shell)
788 .arg(&flag)
789 .arg(command)
790 .env("LEAN_CTX_ACTIVE", "1")
791 .status()
792 .map_or(127, |s| s.code().unwrap_or(1));
793 std::process::exit(status);
794}
795
796fn run_async<F: std::future::Future>(future: F) -> F::Output {
797 tokio::runtime::Runtime::new()
798 .expect("failed to create async runtime")
799 .block_on(future)
800}
801
802fn run_mcp_server() -> Result<()> {
803 use rmcp::ServiceExt;
804 use tracing_subscriber::EnvFilter;
805
806 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
807
808 let rt = tokio::runtime::Runtime::new()?;
809 rt.block_on(async {
810 tracing_subscriber::fmt()
811 .with_env_filter(EnvFilter::from_default_env())
812 .with_writer(std::io::stderr)
813 .init();
814
815 tracing::info!(
816 "lean-ctx v{} MCP server starting",
817 env!("CARGO_PKG_VERSION")
818 );
819
820 let server = tools::create_server();
821 let transport =
822 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
823 let service = match server.serve(transport).await {
824 Ok(s) => s,
825 Err(e) => {
826 let msg = e.to_string();
827 if msg.contains("expect initialized")
828 || msg.contains("context canceled")
829 || msg.contains("broken pipe")
830 {
831 tracing::debug!("Client disconnected before init: {msg}");
832 return Ok(());
833 }
834 return Err(e.into());
835 }
836 };
837 service.waiting().await?;
838
839 core::stats::flush();
840 core::mode_predictor::ModePredictor::flush();
841 core::feedback::FeedbackStore::flush();
842
843 Ok(())
844 })
845}
846
847fn print_help() {
848 println!(
849 "lean-ctx {version} — Context Runtime for AI Agents
850
85190+ compression patterns | 46 MCP tools | Context Continuity Protocol
852
853USAGE:
854 lean-ctx Start MCP server (stdio)
855 lean-ctx serve Start MCP server (Streamable HTTP)
856 lean-ctx -t \"command\" Track command (full output + stats, no compression)
857 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
858 lean-ctx -c --raw \"command\" Execute without compression (full output)
859 lean-ctx exec \"command\" Same as -c
860 lean-ctx shell Interactive shell with compression
861
862COMMANDS:
863 gain Visual dashboard (colors, bars, sparklines, USD)
864 gain --live Live mode: auto-refreshes every 1s in-place
865 gain --graph 30-day savings chart
866 gain --daily Bordered day-by-day table with USD
867 gain --json Raw JSON export of all stats
868 token-report [--json] Token + memory report (project + session + CEP)
869 cep CEP impact report (score trends, cache, modes)
870 watch Live TUI dashboard (real-time event stream)
871 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
872 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
873 proxy start [--port=4444] API proxy: compress tool_results before LLM API
874 proxy status Show proxy statistics
875 cache [list|clear|stats] Show/manage file read cache
876 wrapped [--week|--month|--all] Savings report card (shareable)
877 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
878 benchmark run [path] [--json] Run real benchmark on project files
879 benchmark report [path] Generate shareable Markdown report
880 cheatsheet Command cheat sheet & workflow quick reference
881 setup One-command setup: shell + editor + verify
882 bootstrap Non-interactive setup + fix (zero-config)
883 status [--json] Show setup + MCP + rules status
884 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
885 init --agent <name> Configure MCP for specific editor/agent
886 read <file> [-m mode] Read file with compression
887 diff <file1> <file2> Compressed file diff
888 grep <pattern> [path] Search with compressed output
889 find <pattern> [path] Find files with compressed output
890 ls [path] Directory listing with compression
891 deps [path] Show project dependencies
892 discover Find uncompressed commands in shell history
893 ghost [--json] Ghost Token report: find hidden token waste
894 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
895 session Show adoption statistics
896 config Show/edit configuration (~/.lean-ctx/config.toml)
897 theme [list|set|export|import] Customize terminal colors and themes
898 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
899 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
900 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
901 update [--check] Self-update lean-ctx binary from GitHub Releases
902 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
903 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
904 doctor [--fix] [--json] Run diagnostics (and optionally repair)
905 uninstall Remove shell hook, MCP configs, and data directory
906
907SHELL HOOK PATTERNS (90+):
908 git status, log, diff, add, commit, push, pull, fetch, clone,
909 branch, checkout, switch, merge, stash, tag, reset, remote
910 docker build, ps, images, logs, compose, exec, network
911 npm/pnpm install, test, run, list, outdated, audit
912 cargo build, test, check, clippy
913 gh pr list/view/create, issue list/view, run list/view
914 kubectl get pods/services/deployments, logs, describe, apply
915 python pip install/list/outdated, ruff check/format, poetry, uv
916 linters eslint, biome, prettier, golangci-lint
917 builds tsc, next build, vite build
918 ruby rubocop, bundle install/update, rake test, rails test
919 tests jest, vitest, pytest, go test, playwright, rspec, minitest
920 iac terraform, make, maven, gradle, dotnet, flutter, dart
921 utils curl, grep/rg, find, ls, wget, env
922 data JSON schema extraction, log deduplication
923
924READ MODES:
925 auto Auto-select optimal mode (default)
926 full Full content (cached re-reads = 13 tokens)
927 map Dependency graph + API signatures
928 signatures tree-sitter AST extraction (18 languages)
929 task Task-relevant filtering (requires ctx_session task)
930 reference One-line reference stub (cheap cache key)
931 aggressive Syntax-stripped content
932 entropy Shannon entropy filtered
933 diff Changed lines only
934 lines:N-M Specific line ranges (e.g. lines:10-50,80)
935
936ENVIRONMENT:
937 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
938 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
939 LEAN_CTX_RAW=1 Same as --raw for current command
940 LEAN_CTX_AUTONOMY=false Disable autonomous features
941 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
942
943OPTIONS:
944 --version, -V Show version
945 --help, -h Show this help
946
947EXAMPLES:
948 lean-ctx -c \"git status\" Compressed git output
949 lean-ctx -c \"kubectl get pods\" Compressed k8s output
950 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
951 lean-ctx gain Visual terminal dashboard
952 lean-ctx gain --live Live auto-updating terminal dashboard
953 lean-ctx gain --graph 30-day savings chart
954 lean-ctx gain --daily Day-by-day breakdown with USD
955 lean-ctx token-report --json Machine-readable token + memory report
956 lean-ctx dashboard Open web dashboard at localhost:3333
957 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
958 lean-ctx wrapped Weekly savings report card
959 lean-ctx wrapped --month Monthly savings report card
960 lean-ctx sessions list List all CCP sessions
961 lean-ctx sessions show Show latest session state
962 lean-ctx discover Find missed savings in shell history
963 lean-ctx setup One-command setup (shell + editors + verify)
964 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
965 lean-ctx bootstrap --json Machine-readable bootstrap report
966 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
967 lean-ctx-on Enable shell aliases in track mode (full output + stats)
968 lean-ctx-off Disable all shell aliases
969 lean-ctx-mode track Track mode: full output, stats recorded (default)
970 lean-ctx-mode compress Compress mode: all output compressed (power users)
971 lean-ctx-mode off Same as lean-ctx-off
972 lean-ctx-status Show whether compression is active
973 lean-ctx init --agent pi Install Pi Coding Agent extension
974 lean-ctx doctor Check PATH, config, MCP, and dashboard port
975 lean-ctx doctor --fix --json Repair + machine-readable report
976 lean-ctx status --json Machine-readable current status
977 lean-ctx read src/main.rs -m map
978 lean-ctx grep \"pub fn\" src/
979 lean-ctx deps .
980
981CLOUD:
982 cloud status Show cloud connection status
983 login <email> Log into existing LeanCTX Cloud account
984 register <email> Create a new LeanCTX Cloud account
985 forgot-password <email> Send password reset email
986 sync Upload local stats to cloud dashboard
987 contribute Share anonymized compression data
988
989TROUBLESHOOTING:
990 Commands broken? lean-ctx-off (fixes current session)
991 Permanent fix? lean-ctx uninstall (removes all hooks)
992 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
993 Binary missing? Aliases auto-fallback to original commands (safe)
994 Preview init? lean-ctx init --global --dry-run
995
996WEBSITE: https://leanctx.com
997GITHUB: https://github.com/yvgude/lean-ctx
998",
999 version = env!("CARGO_PKG_VERSION"),
1000 );
1001}