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
809 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
810
811 let startup_lock = crate::core::startup_guard::try_acquire_lock(
815 "mcp-startup",
816 std::time::Duration::from_secs(3),
817 std::time::Duration::from_secs(30),
818 );
819
820 let parallelism = std::thread::available_parallelism()
821 .map(std::num::NonZeroUsize::get)
822 .unwrap_or(2);
823 let worker_threads = parallelism.clamp(1, 4);
824 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
825
826 let rt = tokio::runtime::Builder::new_multi_thread()
827 .worker_threads(worker_threads)
828 .max_blocking_threads(max_blocking_threads)
829 .enable_all()
830 .build()?;
831
832 let server = tools::create_server();
833 drop(startup_lock);
834
835 rt.block_on(async {
836 core::logging::init_mcp_logging();
837
838 tracing::info!(
839 "lean-ctx v{} MCP server starting",
840 env!("CARGO_PKG_VERSION")
841 );
842
843 let transport =
844 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
845 let service = match server.serve(transport).await {
846 Ok(s) => s,
847 Err(e) => {
848 let msg = e.to_string();
849 if msg.contains("expect initialized")
850 || msg.contains("context canceled")
851 || msg.contains("broken pipe")
852 {
853 tracing::debug!("Client disconnected before init: {msg}");
854 return Ok(());
855 }
856 return Err(e.into());
857 }
858 };
859 match service.waiting().await {
860 Ok(reason) => {
861 tracing::info!("MCP server stopped: {reason:?}");
862 }
863 Err(e) => {
864 let msg = e.to_string();
865 if msg.contains("broken pipe")
866 || msg.contains("connection reset")
867 || msg.contains("context canceled")
868 {
869 tracing::info!("MCP server: transport closed ({msg})");
870 } else {
871 tracing::error!("MCP server error: {msg}");
872 }
873 }
874 }
875
876 core::stats::flush();
877 core::mode_predictor::ModePredictor::flush();
878 core::feedback::FeedbackStore::flush();
879
880 Ok(())
881 })
882}
883
884fn print_help() {
885 println!(
886 "lean-ctx {version} — Context Runtime for AI Agents
887
88890+ compression patterns | 49 MCP tools | Context Continuity Protocol
889
890USAGE:
891 lean-ctx Start MCP server (stdio)
892 lean-ctx serve Start MCP server (Streamable HTTP)
893 lean-ctx -t \"command\" Track command (full output + stats, no compression)
894 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
895 lean-ctx -c --raw \"command\" Execute without compression (full output)
896 lean-ctx exec \"command\" Same as -c
897 lean-ctx shell Interactive shell with compression
898
899COMMANDS:
900 gain Visual dashboard (colors, bars, sparklines, USD)
901 gain --live Live mode: auto-refreshes every 1s in-place
902 gain --graph 30-day savings chart
903 gain --daily Bordered day-by-day table with USD
904 gain --json Raw JSON export of all stats
905 token-report [--json] Token + memory report (project + session + CEP)
906 cep CEP impact report (score trends, cache, modes)
907 watch Live TUI dashboard (real-time event stream)
908 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
909 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
910 proxy start [--port=4444] API proxy: compress tool_results before LLM API
911 proxy status Show proxy statistics
912 cache [list|clear|stats] Show/manage file read cache
913 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
914 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
915 benchmark run [path] [--json] Run real benchmark on project files
916 benchmark report [path] Generate shareable Markdown report
917 cheatsheet Command cheat sheet & workflow quick reference
918 setup One-command setup: shell + editor + verify
919 bootstrap Non-interactive setup + fix (zero-config)
920 status [--json] Show setup + MCP + rules status
921 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
922 init --agent <name> Configure MCP for specific editor/agent
923 read <file> [-m mode] Read file with compression
924 diff <file1> <file2> Compressed file diff
925 grep <pattern> [path] Search with compressed output
926 find <pattern> [path] Find files with compressed output
927 ls [path] Directory listing with compression
928 deps [path] Show project dependencies
929 discover Find uncompressed commands in shell history
930 ghost [--json] Ghost Token report: find hidden token waste
931 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
932 session Show adoption statistics
933 config Show/edit configuration (~/.lean-ctx/config.toml)
934 profile [list|show|diff|create|set] Manage context profiles
935 theme [list|set|export|import] Customize terminal colors and themes
936 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
937 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
938 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
939 update [--check] Self-update lean-ctx binary from GitHub Releases
940 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
941 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
942 doctor [--fix] [--json] Run diagnostics (and optionally repair)
943 uninstall Remove shell hook, MCP configs, and data directory
944
945SHELL HOOK PATTERNS (90+):
946 git status, log, diff, add, commit, push, pull, fetch, clone,
947 branch, checkout, switch, merge, stash, tag, reset, remote
948 docker build, ps, images, logs, compose, exec, network
949 npm/pnpm install, test, run, list, outdated, audit
950 cargo build, test, check, clippy
951 gh pr list/view/create, issue list/view, run list/view
952 kubectl get pods/services/deployments, logs, describe, apply
953 python pip install/list/outdated, ruff check/format, poetry, uv
954 linters eslint, biome, prettier, golangci-lint
955 builds tsc, next build, vite build
956 ruby rubocop, bundle install/update, rake test, rails test
957 tests jest, vitest, pytest, go test, playwright, rspec, minitest
958 iac terraform, make, maven, gradle, dotnet, flutter, dart
959 utils curl, grep/rg, find, ls, wget, env
960 data JSON schema extraction, log deduplication
961
962READ MODES:
963 auto Auto-select optimal mode (default)
964 full Full content (cached re-reads = 13 tokens)
965 map Dependency graph + API signatures
966 signatures tree-sitter AST extraction (18 languages)
967 task Task-relevant filtering (requires ctx_session task)
968 reference One-line reference stub (cheap cache key)
969 aggressive Syntax-stripped content
970 entropy Shannon entropy filtered
971 diff Changed lines only
972 lines:N-M Specific line ranges (e.g. lines:10-50,80)
973
974ENVIRONMENT:
975 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
976 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
977 LEAN_CTX_RAW=1 Same as --raw for current command
978 LEAN_CTX_AUTONOMY=false Disable autonomous features
979 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
980
981OPTIONS:
982 --version, -V Show version
983 --help, -h Show this help
984
985EXAMPLES:
986 lean-ctx -c \"git status\" Compressed git output
987 lean-ctx -c \"kubectl get pods\" Compressed k8s output
988 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
989 lean-ctx gain Visual terminal dashboard
990 lean-ctx gain --live Live auto-updating terminal dashboard
991 lean-ctx gain --graph 30-day savings chart
992 lean-ctx gain --daily Day-by-day breakdown with USD
993 lean-ctx token-report --json Machine-readable token + memory report
994 lean-ctx dashboard Open web dashboard at localhost:3333
995 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
996 lean-ctx gain --wrapped Wrapped report card (recommended)
997 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
998 lean-ctx sessions list List all CCP sessions
999 lean-ctx sessions show Show latest session state
1000 lean-ctx discover Find missed savings in shell history
1001 lean-ctx setup One-command setup (shell + editors + verify)
1002 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1003 lean-ctx bootstrap --json Machine-readable bootstrap report
1004 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1005 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1006 lean-ctx-off Disable all shell aliases
1007 lean-ctx-mode track Track mode: full output, stats recorded (default)
1008 lean-ctx-mode compress Compress mode: all output compressed (power users)
1009 lean-ctx-mode off Same as lean-ctx-off
1010 lean-ctx-status Show whether compression is active
1011 lean-ctx init --agent pi Install Pi Coding Agent extension
1012 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1013 lean-ctx doctor --fix --json Repair + machine-readable report
1014 lean-ctx status --json Machine-readable current status
1015 lean-ctx read src/main.rs -m map
1016 lean-ctx grep \"pub fn\" src/
1017 lean-ctx deps .
1018
1019CLOUD:
1020 cloud status Show cloud connection status
1021 login <email> Log into existing LeanCTX Cloud account
1022 register <email> Create a new LeanCTX Cloud account
1023 forgot-password <email> Send password reset email
1024 sync Upload local stats to cloud dashboard
1025 contribute Share anonymized compression data
1026
1027TROUBLESHOOTING:
1028 Commands broken? lean-ctx-off (fixes current session)
1029 Permanent fix? lean-ctx uninstall (removes all hooks)
1030 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1031 Binary missing? Aliases auto-fallback to original commands (safe)
1032 Preview init? lean-ctx init --global --dry-run
1033
1034WEBSITE: https://leanctx.com
1035GITHUB: https://github.com/yvgude/lean-ctx
1036",
1037 version = env!("CARGO_PKG_VERSION"),
1038 );
1039}