1use crate::{
2 core, doctor, heatmap, hook_handlers, report, setup, shell, status, token_report, uninstall,
3};
4
5mod analytics;
6mod help;
7mod lifecycle;
8mod network;
9mod server;
10
11#[allow(clippy::wildcard_imports)]
12use analytics::*;
13#[allow(clippy::wildcard_imports)]
14use help::*;
15#[allow(clippy::wildcard_imports)]
16use lifecycle::*;
17#[allow(clippy::wildcard_imports)]
18use network::*;
19#[allow(clippy::wildcard_imports)]
20use server::*;
21
22pub fn run() {
23 let mut args: Vec<String> = std::env::args().collect();
24
25 if args.get(1).is_some_and(|a| a == "(deleted)") {
29 args.remove(1);
30 }
31
32 if !is_server_mode(&args) {
33 restore_sigpipe_default();
34 }
35
36 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
37 if !enters_mcp {
38 crate::core::logging::init_logging();
39 }
40
41 if args.len() > 1 {
42 let rest = args[2..].to_vec();
43
44 match args[1].as_str() {
45 "-c" | "exec" => {
46 let raw = rest.first().is_some_and(|a| a == "--raw");
47 let cmd_args = if raw { &args[3..] } else { &args[2..] };
48 let command = if cmd_args.len() == 1 {
49 cmd_args[0].clone()
50 } else {
51 shell::join_command(cmd_args)
52 };
53 core::agent_runtime_env::capture();
57 if crate::shell::reentry::should_pass_through() {
58 passthrough(&command);
59 }
60 if raw {
61 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
64 } else {
65 unsafe { std::env::set_var("LEAN_CTX_COMPRESS", "1") };
68 }
69 let code = shell::exec(&command);
70 core::tool_lifecycle::flush_all();
71 std::process::exit(code);
72 }
73 "-t" | "--track" => {
74 let cmd_args = &args[2..];
75 let code = if cmd_args.len() > 1 {
76 shell::exec_argv(cmd_args)
77 } else {
78 let command = cmd_args[0].clone();
79 if crate::shell::reentry::should_pass_through() {
80 passthrough(&command);
81 }
82 shell::exec(&command)
83 };
84 core::tool_lifecycle::flush_all();
85 std::process::exit(code);
86 }
87 "shell" | "--shell" => {
88 shell::interactive();
89 return;
90 }
91 "gain" => {
92 cmd_gain(&rest);
93 return;
94 }
95 "spend" => {
96 cmd_spend(&rest);
97 return;
98 }
99 "savings" => {
100 cmd_savings(&rest);
101 return;
102 }
103 "learning" => {
104 cmd_learning(&rest);
105 return;
106 }
107 "conformance" | "selftest" => {
108 cmd_conformance(&rest);
109 return;
110 }
111 "billing" => {
112 cmd_billing(&rest);
113 return;
114 }
115 "finops" => {
116 cmd_finops(&rest);
117 return;
118 }
119 "roi" => {
120 super::cmd_roi(&rest);
123 return;
124 }
125 "output-savings" | "output_savings" => {
126 super::cmd_output_savings(&rest);
129 return;
130 }
131 "token-report" | "report-tokens" => {
132 let code = token_report::run_cli(&rest);
133 if code != 0 {
134 std::process::exit(code);
135 }
136 return;
137 }
138 "pack" => {
139 crate::cli::cmd_pack(&rest);
140 return;
141 }
142 "policy" => {
143 crate::cli::cmd_policy(&rest);
144 return;
145 }
146 "plugin" | "plugins" => {
147 crate::cli::plugin_cmd::cmd_plugin(&rest);
148 return;
149 }
150 "addon" | "addons" => {
151 crate::cli::addon_cmd::cmd_addon(&rest);
152 return;
153 }
154 "rules" => {
155 crate::cli::rules_cmd::cmd_rules(&rest);
156 return;
157 }
158 "proof" => {
159 crate::cli::cmd_proof(&rest);
160 return;
161 }
162 "verify" => {
163 crate::cli::cmd_verify(&rest);
164 return;
165 }
166 "eval" => {
167 crate::cli::eval_cmd::cmd_eval(&rest);
168 return;
169 }
170 "verify-cache" | "cache-selftest" => {
171 let code = crate::cli::verify_cache_cmd::cmd_verify_cache(&rest);
172 if code != 0 {
173 std::process::exit(code);
174 }
175 return;
176 }
177 "visualize" => {
178 super::cmd_visualize(&rest);
179 return;
180 }
181 "audit" => {
182 if rest.first().map(String::as_str) == Some("evidence") {
183 crate::cli::audit_report::cmd_evidence(&rest[1..]);
184 } else {
185 println!("{}", crate::cli::audit_report::generate_report());
186 }
187 return;
188 }
189 "compliance" => {
190 crate::cli::cmd_compliance(&rest);
191 return;
192 }
193 "agent" => {
194 crate::cli::cmd_agent(&rest);
195 return;
196 }
197 "instructions" => {
198 crate::cli::cmd_instructions(&rest);
199 return;
200 }
201 "index" => {
202 crate::cli::cmd_index(&rest);
203 return;
204 }
205 "semantic-search" | "search-code" => {
206 crate::cli::cmd_semantic_search(&rest);
207 core::stats::flush();
208 return;
209 }
210 "explore" => {
211 crate::cli::explore_cmd::cmd_explore(&rest);
212 core::stats::flush();
213 return;
214 }
215 "repomap" | "repo-map" => {
216 crate::cli::cmd_repomap(&rest);
217 core::stats::flush();
218 return;
219 }
220 "cep" => {
221 println!("{}", core::stats::format_cep_report());
222 return;
223 }
224 "dashboard" => {
225 cmd_dashboard(&rest);
226 return;
227 }
228 "team" => {
229 cmd_team(&rest);
230 return;
231 }
232 "provider" => {
233 cmd_provider(&rest);
234 return;
235 }
236 "serve" => {
237 cmd_serve(&rest);
238 return;
239 }
240 "watch" => {
241 cmd_watch(&rest);
242 return;
243 }
244 "proxy" => {
245 cmd_proxy(&rest);
246 return;
247 }
248 "daemon" => {
249 cmd_daemon(&rest);
250 return;
251 }
252 "init" => {
253 super::cmd_init(&rest);
254 return;
255 }
256 "setup" => {
257 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
258 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
259 let fix = rest.iter().any(|a| a == "--fix");
260 let json = rest.iter().any(|a| a == "--json");
261 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
262 let skip_rules = rest.iter().any(|a| a == "--skip-rules");
263
264 if non_interactive || fix || json || yes {
265 let opts = setup::SetupOptions {
266 non_interactive,
267 yes,
268 fix,
269 json,
270 no_auto_approve,
271 skip_rules,
272 ..Default::default()
273 };
274 match setup::run_setup_with_options(opts) {
275 Ok(report) => {
276 if json {
277 println!(
278 "{}",
279 serde_json::to_string_pretty(&report)
280 .unwrap_or_else(|_| "{}".to_string())
281 );
282 }
283 if !report.success {
284 std::process::exit(1);
285 }
286 }
287 Err(e) => {
288 eprintln!("{e}");
289 std::process::exit(1);
290 }
291 }
292 } else {
293 setup::run_setup();
294 }
295 return;
296 }
297 "onboard" => {
298 setup::run_onboard();
299 return;
300 }
301 "install" => {
302 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
307 let json = rest.iter().any(|a| a == "--json");
308 if !repair {
309 setup::run_setup();
310 return;
311 }
312 let opts = setup::SetupOptions {
313 non_interactive: true,
314 yes: true,
315 fix: true,
316 json,
317 ..Default::default()
318 };
319 match setup::run_setup_with_options(opts) {
320 Ok(report) => {
321 if json {
322 println!(
323 "{}",
324 serde_json::to_string_pretty(&report)
325 .unwrap_or_else(|_| "{}".to_string())
326 );
327 }
328 if !report.success {
329 std::process::exit(1);
330 }
331 }
332 Err(e) => {
333 eprintln!("{e}");
334 std::process::exit(1);
335 }
336 }
337 return;
338 }
339 "bootstrap" => {
340 let json = rest.iter().any(|a| a == "--json");
341 let opts = setup::SetupOptions {
342 non_interactive: true,
343 yes: true,
344 fix: true,
345 json,
346 ..Default::default()
347 };
348 match setup::run_setup_with_options(opts) {
349 Ok(report) => {
350 if json {
351 println!(
352 "{}",
353 serde_json::to_string_pretty(&report)
354 .unwrap_or_else(|_| "{}".to_string())
355 );
356 }
357 if !report.success {
358 std::process::exit(1);
359 }
360 }
361 Err(e) => {
362 eprintln!("{e}");
363 std::process::exit(1);
364 }
365 }
366 return;
367 }
368 "status" => {
369 let code = status::run_cli(&rest);
370 if code != 0 {
371 std::process::exit(code);
372 }
373 return;
374 }
375 "read" => {
376 super::cmd_read(&rest);
377 core::tool_lifecycle::flush_all();
378 return;
379 }
380 "call" => {
381 super::cmd_call(&rest);
382 return;
383 }
384 "diff" => {
385 super::cmd_diff(&rest);
386 core::tool_lifecycle::flush_all();
387 return;
388 }
389 "grep" => {
390 super::cmd_grep(&rest);
391 core::tool_lifecycle::flush_all();
392 return;
393 }
394 "glob" => {
395 super::cmd_glob(&rest);
396 core::stats::flush();
397 return;
398 }
399 "find" => {
400 super::cmd_find(&rest);
401 core::tool_lifecycle::flush_all();
402 return;
403 }
404 "ls" => {
405 super::cmd_ls(&rest);
406 core::tool_lifecycle::flush_all();
407 return;
408 }
409 "deps" => {
410 super::cmd_deps(&rest);
411 core::tool_lifecycle::flush_all();
412 return;
413 }
414 "discover" => {
415 super::cmd_discover(&rest);
416 return;
417 }
418 "ghost" => {
419 super::cmd_ghost(&rest);
420 return;
421 }
422 "filter" => {
423 super::cmd_filter(&rest);
424 return;
425 }
426 "heatmap" => {
427 heatmap::cmd_heatmap(&rest);
428 return;
429 }
430 "graph" => {
431 cmd_graph(&rest);
432 return;
433 }
434 "smells" => {
435 cmd_smells(&rest);
436 return;
437 }
438 "session" => {
439 super::cmd_session_action(&rest);
440 return;
441 }
442 "ledger" => {
443 super::cmd_ledger(&rest);
444 return;
445 }
446 "control" | "context-control" => {
447 super::cmd_control(&rest);
448 return;
449 }
450 "plan" | "context-plan" => {
451 super::cmd_plan(&rest);
452 return;
453 }
454 "compile" | "context-compile" => {
455 super::cmd_compile(&rest);
456 return;
457 }
458 "knowledge" => {
459 super::cmd_knowledge(&rest);
460 return;
461 }
462 "skillify" => {
463 super::cmd_skillify(&rest);
464 return;
465 }
466 "summary" => {
467 super::cmd_summary(&rest);
468 return;
469 }
470 "overview" => {
471 super::cmd_overview(&rest);
472 return;
473 }
474 "compress" => {
475 super::cmd_compress(&rest);
476 return;
477 }
478 "wrapped" => {
479 eprintln!("'lean-ctx wrapped' has been removed. Use: lean-ctx gain --wrapped");
480 std::process::exit(1);
481 }
482 "sessions" | "session-store" => {
483 super::cmd_sessions(&rest);
484 return;
485 }
486 "benchmark" => {
487 super::cmd_benchmark(&rest);
488 return;
489 }
490 "compact" => {
491 cmd_compact(&rest);
492 return;
493 }
494 "profile" => {
495 super::cmd_profile(&rest);
496 return;
497 }
498 "tools" => {
499 if rest.first().map(String::as_str) == Some("health") {
502 super::cmd_tools_health(&rest[1..]);
503 return;
504 }
505 let mut forwarded = vec!["tools".to_string()];
509 forwarded.extend(rest.iter().cloned());
510 super::cmd_profile(&forwarded);
511 return;
512 }
513 "config" => {
514 super::cmd_config(&rest);
515 return;
516 }
517 "allow" => {
518 super::cmd_allow(&rest);
519 return;
520 }
521 "security" => {
522 super::cmd_security(&rest);
523 return;
524 }
525 "yolo" => {
526 super::cmd_yolo(&rest);
527 return;
528 }
529 "secure" | "lockdown" => {
530 super::cmd_secure(&rest);
531 return;
532 }
533 "trust" => {
534 super::cmd_trust(&rest);
535 return;
536 }
537 "untrust" => {
538 super::cmd_untrust(&rest);
539 return;
540 }
541 "stats" => {
542 super::cmd_stats(&rest);
543 return;
544 }
545 "introspect" => {
546 super::cmd_introspect(&rest);
547 return;
548 }
549 "cache" => {
550 super::cmd_cache(&rest);
551 return;
552 }
553 "theme" => {
554 super::cmd_theme(&rest);
555 return;
556 }
557 "tee" => {
558 super::cmd_tee(&rest);
559 return;
560 }
561 "terse" | "compression" => {
562 super::cmd_compression(&rest);
563 return;
564 }
565 "slow-log" => {
566 super::cmd_slow_log(&rest);
567 return;
568 }
569 "debug-log" => {
570 super::cmd_debug_log(&rest);
571 return;
572 }
573 "editor-signal" => {
576 let file = rest
577 .iter()
578 .position(|a| a == "--file")
579 .and_then(|i| rest.get(i + 1));
580 if let Some(path) = file {
581 if let Err(e) = core::editor_signal::record_focus(path) {
582 eprintln!("editor-signal: {e}");
583 std::process::exit(1);
584 }
585 } else {
586 eprintln!("usage: lean-ctx editor-signal --file <path>");
587 std::process::exit(2);
588 }
589 return;
590 }
591 "update" | "--self-update" => {
592 core::updater::run(&rest);
593 return;
594 }
595 "restart" => {
596 cmd_restart();
597 return;
598 }
599 "stop" => {
600 cmd_stop();
601 return;
602 }
603 "dev-install" => {
604 cmd_dev_install();
605 return;
606 }
607 "codesign-setup" => {
608 cmd_codesign_setup();
609 return;
610 }
611 "doctor" => {
612 let code = doctor::run_cli(&rest);
613 if code != 0 {
614 std::process::exit(code);
615 }
616 return;
617 }
618 "harden" => {
619 super::harden::run(&rest);
620 return;
621 }
622 "export-rules" => {
623 super::export_rules::run(&rest);
624 return;
625 }
626 "gotchas" | "bugs" => {
627 super::cloud::cmd_gotchas(&rest);
628 return;
629 }
630 "learn" => {
631 super::cmd_learn(&rest);
632 return;
633 }
634 "buddy" | "pet" => {
635 super::cloud::cmd_buddy(&rest);
636 return;
637 }
638 "hook" => {
639 hook_handlers::mark_hook_environment();
640 core::agent_runtime_env::capture();
644 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
645 let action = rest.first().map_or("help", std::string::String::as_str);
646 match action {
647 "rewrite" => hook_handlers::handle_rewrite(),
648 "redirect" => hook_handlers::handle_redirect(),
649 "observe" => hook_handlers::handle_observe(),
650 "copilot" => hook_handlers::handle_copilot(),
651 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
652 "codex-session-start" => hook_handlers::handle_codex_session_start(),
653 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
654 _ => {
655 eprintln!(
656 "Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>"
657 );
658 eprintln!(
659 " Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)"
660 );
661 std::process::exit(1);
662 }
663 }
664 return;
665 }
666 "report-issue" | "report" => {
667 report::run(&rest);
668 return;
669 }
670 "uninstall" => {
671 if rest.iter().any(|a| a == "--help" || a == "-h") {
674 uninstall::print_help();
675 return;
676 }
677 let dry_run = rest.iter().any(|a| a == "--dry-run");
678 let keep_config = rest.iter().any(|a| a == "--keep-config");
679 let keep_binary = rest.iter().any(|a| a == "--keep-binary");
680 uninstall::run(dry_run, keep_config, keep_binary);
681 return;
682 }
683 "raw" | "bypass" => {
688 if rest.is_empty() {
689 eprintln!("Usage: lean-ctx raw \"command\"");
690 eprintln!(
691 "Runs the command with output passed through unchanged (no \
692 compression). The shell allowlist still applies."
693 );
694 std::process::exit(1);
695 }
696 let command = if rest.len() == 1 {
697 rest[0].clone()
698 } else {
699 shell::join_command(&args[2..])
700 };
701 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
704 let code = shell::exec(&command);
705 std::process::exit(code);
706 }
707 "safety-levels" | "safety" => {
708 println!("{}", core::compression_safety::format_safety_table());
709 return;
710 }
711 "cheat" | "cheatsheet" | "cheat-sheet" => {
712 super::cmd_cheatsheet();
713 return;
714 }
715 "login" => {
716 super::cloud::cmd_login(&rest);
717 return;
718 }
719 "register" => {
720 super::cloud::cmd_register(&rest);
721 return;
722 }
723 "forgot-password" => {
724 super::cloud::cmd_forgot_password(&rest);
725 return;
726 }
727 "sync" => {
728 super::cloud::cmd_sync(&rest);
729 return;
730 }
731 "contribute" => {
732 super::cloud::cmd_contribute();
733 return;
734 }
735 "cloud" => {
736 super::cloud::cmd_cloud(&rest);
737 return;
738 }
739 "upgrade" => {
740 super::cloud::cmd_upgrade();
741 return;
742 }
743 "--version" | "-V" => {
744 println!("{}", core::integrity::origin_line());
745 return;
746 }
747 "help" => {
748 let want_all = rest
749 .iter()
750 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"));
751 if want_all {
752 print_help();
753 } else {
754 print_help_concise();
755 }
756 return;
757 }
758 "--help" | "-h" => {
759 if rest
760 .iter()
761 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"))
762 {
763 print_help();
764 } else {
765 print_help_concise();
766 }
767 return;
768 }
769 "mcp" => {}
770 _ => {
771 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
772 print_help_concise();
773 std::process::exit(1);
774 }
775 }
776 }
777
778 if args.len() == 1 && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
784 print_quickstart();
785 return;
786 }
787
788 if let Err(e) = run_mcp_server() {
789 tracing::error!("lean-ctx: {e}");
790 std::process::exit(1);
791 }
792}
793
794fn is_server_mode(args: &[String]) -> bool {
798 args.len() == 1
799 || args.get(1).is_some_and(|a| {
800 matches!(
801 a.as_str(),
802 "mcp" | "daemon" | "proxy" | "serve" | "watch" | "dashboard"
803 )
804 })
805}
806
807#[cfg(unix)]
817fn restore_sigpipe_default() {
818 unsafe {
821 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
822 }
823}
824
825#[cfg(not(unix))]
826fn restore_sigpipe_default() {}
827
828fn passthrough(command: &str) -> ! {
829 let (shell, flag) = shell::shell_and_flag();
830 let mut cmd = std::process::Command::new(&shell);
831 cmd.arg(&flag).arg(command);
832 shell::reentry::mark_child(&mut cmd);
833 shell::platform::apply_utf8_locale(&mut cmd);
834 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
835 std::process::exit(status);
836}
837
838pub(super) fn run_async<F: std::future::Future>(future: F) -> F::Output {
839 tokio::runtime::Runtime::new()
840 .expect("failed to create async runtime")
841 .block_on(future)
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use serial_test::serial;
848
849 fn args_of(parts: &[&str]) -> Vec<String> {
850 parts.iter().map(|s| (*s).to_string()).collect()
851 }
852
853 #[test]
854 fn server_modes_keep_ignored_sigpipe() {
855 for mode in ["mcp", "daemon", "proxy", "serve", "watch", "dashboard"] {
856 assert!(
857 is_server_mode(&args_of(&["lean-ctx", mode])),
858 "{mode} must count as server mode"
859 );
860 }
861 assert!(is_server_mode(&args_of(&["lean-ctx"])));
863 }
864
865 #[test]
866 fn cli_modes_restore_default_sigpipe() {
867 for mode in ["doctor", "-c", "status", "ls", "grep", "gain", "help"] {
868 assert!(
869 !is_server_mode(&args_of(&["lean-ctx", mode])),
870 "{mode} must count as CLI mode (SIGPIPE default)"
871 );
872 }
873 }
874
875 #[test]
876 fn quickstart_is_short_and_points_to_setup() {
877 let q = quickstart_text();
878 assert!(
879 q.contains("lean-ctx onboard"),
880 "quickstart must point to onboard"
881 );
882 assert!(q.contains("lean-ctx help"), "quickstart must point to help");
883 assert!(
885 q.lines().count() <= 16,
886 "quickstart should be short; got {} lines",
887 q.lines().count()
888 );
889 assert!(
890 !q.contains("COMMANDS:"),
891 "quickstart must not inline the full command reference"
892 );
893 }
894
895 #[test]
896 fn concise_help_is_short_and_points_to_full() {
897 let h = concise_help_text();
898 assert!(h.contains("lean-ctx onboard"), "must lead with onboard");
899 assert!(
900 h.contains("lean-ctx help all"),
901 "must point to full reference"
902 );
903 assert!(
904 h.contains("lean-ctx tools"),
905 "must surface the tools profile command"
906 );
907 assert!(
909 h.lines().count() <= 40,
910 "concise help should stay short; got {} lines",
911 h.lines().count()
912 );
913 assert!(
914 !h.contains("SHELL HOOK PATTERNS"),
915 "concise help must not inline the full pattern catalog"
916 );
917 }
918
919 #[test]
920 fn capability_banner_tool_count_matches_registry() {
921 let n = crate::server::registry::tool_count();
922 let banner = capability_banner();
923 assert!(
924 banner.contains(&format!("{n} MCP tools")),
925 "banner must show the live registry count ({n}); got: {banner}"
926 );
927 }
928
929 #[test]
930 #[serial]
931 fn worker_threads_default_clamps_low() {
932 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
933 assert_eq!(resolve_worker_threads(1), 1);
934 }
935
936 #[test]
937 #[serial]
938 fn worker_threads_default_clamps_high() {
939 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
940 assert_eq!(resolve_worker_threads(32), 4);
941 }
942
943 #[test]
944 #[serial]
945 fn worker_threads_default_passthrough() {
946 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
947 assert_eq!(resolve_worker_threads(3), 3);
948 }
949
950 #[test]
951 #[serial]
952 fn worker_threads_env_override() {
953 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "12");
954 assert_eq!(resolve_worker_threads(2), 12);
955 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
956 }
957
958 #[test]
959 #[serial]
960 fn worker_threads_env_invalid_falls_back() {
961 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
962 assert_eq!(resolve_worker_threads(3), 3);
963 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
964 }
965}