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