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 mut args: Vec<String> = std::env::args().collect();
9
10 if args.get(1).is_some_and(|a| a == "(deleted)") {
14 args.remove(1);
15 }
16
17 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
18 if !enters_mcp {
19 crate::core::logging::init_logging();
20 }
21
22 if args.len() > 1 {
23 let rest = args[2..].to_vec();
24
25 match args[1].as_str() {
26 "-c" | "exec" => {
27 let raw = rest.first().is_some_and(|a| a == "--raw");
28 let cmd_args = if raw { &args[3..] } else { &args[2..] };
29 let command = if cmd_args.len() == 1 {
30 cmd_args[0].clone()
31 } else {
32 shell::join_command(cmd_args)
33 };
34 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
35 || std::env::var("LEAN_CTX_DISABLED").is_ok()
36 {
37 passthrough(&command);
38 }
39 if raw {
40 std::env::set_var("LEAN_CTX_RAW", "1");
41 } else {
42 std::env::set_var("LEAN_CTX_COMPRESS", "1");
43 }
44 let code = shell::exec(&command);
45 core::stats::flush();
46 core::heatmap::flush();
47 std::process::exit(code);
48 }
49 "-t" | "--track" => {
50 let cmd_args = &args[2..];
51 let code = if cmd_args.len() > 1 {
52 shell::exec_argv(cmd_args)
53 } else {
54 let command = cmd_args[0].clone();
55 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
56 || std::env::var("LEAN_CTX_DISABLED").is_ok()
57 {
58 passthrough(&command);
59 }
60 shell::exec(&command)
61 };
62 core::stats::flush();
63 core::heatmap::flush();
64 std::process::exit(code);
65 }
66 "shell" | "--shell" => {
67 shell::interactive();
68 return;
69 }
70 "gain" => {
71 if rest.iter().any(|a| a == "--reset") {
72 core::stats::reset_all();
73 println!("Stats reset. All token savings data cleared.");
74 return;
75 }
76 if rest.iter().any(|a| a == "--live" || a == "--watch") {
77 core::stats::gain_live();
78 return;
79 }
80 let model = rest.iter().enumerate().find_map(|(i, a)| {
81 if let Some(v) = a.strip_prefix("--model=") {
82 return Some(v.to_string());
83 }
84 if a == "--model" {
85 return rest.get(i + 1).cloned();
86 }
87 None
88 });
89 let period = rest
90 .iter()
91 .enumerate()
92 .find_map(|(i, a)| {
93 if let Some(v) = a.strip_prefix("--period=") {
94 return Some(v.to_string());
95 }
96 if a == "--period" {
97 return rest.get(i + 1).cloned();
98 }
99 None
100 })
101 .unwrap_or_else(|| "all".to_string());
102 let limit = rest
103 .iter()
104 .enumerate()
105 .find_map(|(i, a)| {
106 if let Some(v) = a.strip_prefix("--limit=") {
107 return v.parse::<usize>().ok();
108 }
109 if a == "--limit" {
110 return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
111 }
112 None
113 })
114 .unwrap_or(10);
115
116 if rest.iter().any(|a| a == "--graph") {
117 println!("{}", core::stats::format_gain_graph());
118 } else if rest.iter().any(|a| a == "--daily") {
119 println!("{}", core::stats::format_gain_daily());
120 } else if rest.iter().any(|a| a == "--json") {
121 println!(
122 "{}",
123 tools::ctx_gain::handle(
124 "json",
125 Some(&period),
126 model.as_deref(),
127 Some(limit)
128 )
129 );
130 } else if rest.iter().any(|a| a == "--score") {
131 println!(
132 "{}",
133 tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
134 );
135 } else if rest.iter().any(|a| a == "--cost") {
136 println!(
137 "{}",
138 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
139 );
140 } else if rest.iter().any(|a| a == "--tasks") {
141 println!(
142 "{}",
143 tools::ctx_gain::handle("tasks", None, None, Some(limit))
144 );
145 } else if rest.iter().any(|a| a == "--agents") {
146 println!(
147 "{}",
148 tools::ctx_gain::handle("agents", None, None, Some(limit))
149 );
150 } else if rest.iter().any(|a| a == "--heatmap") {
151 println!(
152 "{}",
153 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
154 );
155 } else if rest.iter().any(|a| a == "--wrapped") {
156 println!(
157 "{}",
158 tools::ctx_gain::handle(
159 "wrapped",
160 Some(&period),
161 model.as_deref(),
162 Some(limit)
163 )
164 );
165 } else if rest.iter().any(|a| a == "--pipeline") {
166 let stats_path = dirs::home_dir()
167 .unwrap_or_default()
168 .join(".lean-ctx")
169 .join("pipeline_stats.json");
170 if let Ok(data) = std::fs::read_to_string(&stats_path) {
171 if let Ok(stats) =
172 serde_json::from_str::<core::pipeline::PipelineStats>(&data)
173 {
174 println!("{}", stats.format_summary());
175 } else {
176 println!("No pipeline stats available yet (corrupt data).");
177 }
178 } else {
179 println!(
180 "No pipeline stats available yet. Use MCP tools to generate data."
181 );
182 }
183 } else if rest.iter().any(|a| a == "--deep") {
184 println!(
185 "{}\n{}\n{}\n{}\n{}",
186 tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
187 tools::ctx_gain::handle("tasks", None, None, Some(limit)),
188 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
189 tools::ctx_gain::handle("agents", None, None, Some(limit)),
190 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
191 );
192 } else {
193 println!("{}", core::stats::format_gain());
194 }
195 return;
196 }
197 "token-report" | "report-tokens" => {
198 let code = token_report::run_cli(&rest);
199 if code != 0 {
200 std::process::exit(code);
201 }
202 return;
203 }
204 "pack" => {
205 crate::cli::cmd_pack(&rest);
206 return;
207 }
208 "proof" => {
209 crate::cli::cmd_proof(&rest);
210 return;
211 }
212 "verify" => {
213 crate::cli::cmd_verify(&rest);
214 return;
215 }
216 "audit" => {
217 println!("{}", crate::cli::audit_report::generate_report());
218 return;
219 }
220 "instructions" => {
221 crate::cli::cmd_instructions(&rest);
222 return;
223 }
224 "index" => {
225 crate::cli::cmd_index(&rest);
226 return;
227 }
228 "cep" => {
229 println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
230 return;
231 }
232 "dashboard" => {
233 if rest.iter().any(|a| a == "--help" || a == "-h") {
234 println!("Usage: lean-ctx dashboard [--port=N] [--host=H] [--project=PATH]");
235 println!("Examples:");
236 println!(" lean-ctx dashboard");
237 println!(" lean-ctx dashboard --port=3333");
238 println!(" lean-ctx dashboard --host=0.0.0.0");
239 return;
240 }
241 let port = rest
242 .iter()
243 .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
244 .and_then(|p| p.parse().ok());
245 let host = rest
246 .iter()
247 .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
248 .map(String::from);
249 let project = rest
250 .iter()
251 .find_map(|p| p.strip_prefix("--project="))
252 .map(String::from);
253 if let Some(ref p) = project {
254 std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
255 }
256 spawn_proxy_if_needed();
257 run_async(dashboard::start(port, host));
258 return;
259 }
260 "team" => {
261 let sub = rest.first().map_or("help", std::string::String::as_str);
262 match sub {
263 "serve" => {
264 #[cfg(feature = "team-server")]
265 {
266 let cfg_path = rest
267 .iter()
268 .enumerate()
269 .find_map(|(i, a)| {
270 if let Some(v) = a.strip_prefix("--config=") {
271 return Some(v.to_string());
272 }
273 if a == "--config" {
274 return rest.get(i + 1).cloned();
275 }
276 None
277 })
278 .unwrap_or_default();
279
280 if cfg_path.trim().is_empty() {
281 eprintln!("Usage: lean-ctx team serve --config <path>");
282 std::process::exit(1);
283 }
284
285 let cfg = crate::http_server::team::TeamServerConfig::load(
286 std::path::Path::new(&cfg_path),
287 )
288 .unwrap_or_else(|e| {
289 eprintln!("Invalid team config: {e}");
290 std::process::exit(1);
291 });
292
293 if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
294 tracing::error!("Team server error: {e}");
295 std::process::exit(1);
296 }
297 return;
298 }
299 #[cfg(not(feature = "team-server"))]
300 {
301 eprintln!("lean-ctx team serve is not available in this build");
302 std::process::exit(1);
303 }
304 }
305 "token" => {
306 let action = rest.get(1).map_or("help", std::string::String::as_str);
307 if action == "create" {
308 #[cfg(feature = "team-server")]
309 {
310 let args = &rest[2..];
311 let cfg_path = args
312 .iter()
313 .enumerate()
314 .find_map(|(i, a)| {
315 if let Some(v) = a.strip_prefix("--config=") {
316 return Some(v.to_string());
317 }
318 if a == "--config" {
319 return args.get(i + 1).cloned();
320 }
321 None
322 })
323 .unwrap_or_default();
324 let token_id = args
325 .iter()
326 .enumerate()
327 .find_map(|(i, a)| {
328 if let Some(v) = a.strip_prefix("--id=") {
329 return Some(v.to_string());
330 }
331 if a == "--id" {
332 return args.get(i + 1).cloned();
333 }
334 None
335 })
336 .unwrap_or_default();
337 let scopes_csv = args
338 .iter()
339 .enumerate()
340 .find_map(|(i, a)| {
341 if let Some(v) = a.strip_prefix("--scopes=") {
342 return Some(v.to_string());
343 }
344 if let Some(v) = a.strip_prefix("--scope=") {
345 return Some(v.to_string());
346 }
347 if a == "--scopes" || a == "--scope" {
348 return args.get(i + 1).cloned();
349 }
350 None
351 })
352 .unwrap_or_default();
353
354 if cfg_path.trim().is_empty()
355 || token_id.trim().is_empty()
356 || scopes_csv.trim().is_empty()
357 {
358 eprintln!(
359 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
360 );
361 std::process::exit(1);
362 }
363
364 let cfg_p = std::path::PathBuf::from(&cfg_path);
365 let mut cfg = crate::http_server::team::TeamServerConfig::load(
366 cfg_p.as_path(),
367 )
368 .unwrap_or_else(|e| {
369 eprintln!("Invalid team config: {e}");
370 std::process::exit(1);
371 });
372
373 let mut scopes = Vec::new();
374 for part in scopes_csv.split(',') {
375 let p = part.trim().to_ascii_lowercase();
376 if p.is_empty() {
377 continue;
378 }
379 let scope = match p.as_str() {
380 "search" => crate::http_server::team::TeamScope::Search,
381 "graph" => crate::http_server::team::TeamScope::Graph,
382 "artifacts" => {
383 crate::http_server::team::TeamScope::Artifacts
384 }
385 "index" => crate::http_server::team::TeamScope::Index,
386 "events" => crate::http_server::team::TeamScope::Events,
387 "sessionmutations" | "session_mutations" => {
388 crate::http_server::team::TeamScope::SessionMutations
389 }
390 "knowledge" => {
391 crate::http_server::team::TeamScope::Knowledge
392 }
393 "audit" => crate::http_server::team::TeamScope::Audit,
394 _ => {
395 eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
396 std::process::exit(1);
397 }
398 };
399 if !scopes.contains(&scope) {
400 scopes.push(scope);
401 }
402 }
403 if scopes.is_empty() {
404 eprintln!("At least 1 scope is required");
405 std::process::exit(1);
406 }
407
408 let (token, hash) = crate::http_server::team::create_token()
409 .unwrap_or_else(|e| {
410 eprintln!("Token generation failed: {e}");
411 std::process::exit(1);
412 });
413
414 cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
415 id: token_id,
416 sha256_hex: hash,
417 scopes,
418 });
419
420 cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
421 eprintln!("Failed to write config: {e}");
422 std::process::exit(1);
423 });
424
425 println!("{token}");
426 return;
427 }
428
429 #[cfg(not(feature = "team-server"))]
430 {
431 eprintln!("lean-ctx team token is not available in this build");
432 std::process::exit(1);
433 }
434 }
435 eprintln!(
436 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
437 );
438 std::process::exit(1);
439 }
440 "sync" => {
441 #[cfg(feature = "team-server")]
442 {
443 let args = &rest[1..];
444 let cfg_path = args
445 .iter()
446 .enumerate()
447 .find_map(|(i, a)| {
448 if let Some(v) = a.strip_prefix("--config=") {
449 return Some(v.to_string());
450 }
451 if a == "--config" {
452 return args.get(i + 1).cloned();
453 }
454 None
455 })
456 .unwrap_or_default();
457 if cfg_path.trim().is_empty() {
458 eprintln!(
459 "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
460 );
461 std::process::exit(1);
462 }
463 let only_ws = args.iter().enumerate().find_map(|(i, a)| {
464 if let Some(v) = a.strip_prefix("--workspace=") {
465 return Some(v.to_string());
466 }
467 if let Some(v) = a.strip_prefix("--workspace-id=") {
468 return Some(v.to_string());
469 }
470 if a == "--workspace" || a == "--workspace-id" {
471 return args.get(i + 1).cloned();
472 }
473 None
474 });
475
476 let cfg = crate::http_server::team::TeamServerConfig::load(
477 std::path::Path::new(&cfg_path),
478 )
479 .unwrap_or_else(|e| {
480 eprintln!("Invalid team config: {e}");
481 std::process::exit(1);
482 });
483
484 for ws in &cfg.workspaces {
485 if let Some(ref only) = only_ws {
486 if ws.id != *only {
487 continue;
488 }
489 }
490 let git_dir = ws.root.join(".git");
491 if !git_dir.exists() {
492 eprintln!(
493 "workspace '{}' root is not a git repo: {}",
494 ws.id,
495 ws.root.display()
496 );
497 std::process::exit(1);
498 }
499 let status = std::process::Command::new("git")
500 .arg("-C")
501 .arg(&ws.root)
502 .args(["fetch", "--all", "--prune"])
503 .status()
504 .unwrap_or_else(|e| {
505 eprintln!(
506 "git fetch failed for workspace '{}': {e}",
507 ws.id
508 );
509 std::process::exit(1);
510 });
511 if !status.success() {
512 eprintln!(
513 "git fetch failed for workspace '{}' (exit={})",
514 ws.id,
515 status.code().unwrap_or(1)
516 );
517 std::process::exit(1);
518 }
519 }
520 return;
521 }
522 #[cfg(not(feature = "team-server"))]
523 {
524 eprintln!("lean-ctx team sync is not available in this build");
525 std::process::exit(1);
526 }
527 }
528 _ => {
529 eprintln!(
530 "Usage:\n lean-ctx team serve --config <path>\n lean-ctx team token create --config <path> --id <id> --scopes <csv>\n lean-ctx team sync --config <path> [--workspace <id>]"
531 );
532 std::process::exit(1);
533 }
534 }
535 }
536 "serve" => {
537 #[cfg(feature = "http-server")]
538 {
539 let mut cfg = crate::http_server::HttpServerConfig::default();
540 let mut daemon_mode = false;
541 let mut stop_mode = false;
542 let mut status_mode = false;
543 let mut foreground_daemon = false;
544 let mut i = 0;
545 while i < rest.len() {
546 match rest[i].as_str() {
547 "--daemon" | "-d" => daemon_mode = true,
548 "--stop" => stop_mode = true,
549 "--status" => status_mode = true,
550 "--_foreground-daemon" => foreground_daemon = true,
551 "--host" | "-H" => {
552 i += 1;
553 if i < rest.len() {
554 cfg.host.clone_from(&rest[i]);
555 }
556 }
557 arg if arg.starts_with("--host=") => {
558 cfg.host = arg["--host=".len()..].to_string();
559 }
560 "--port" | "-p" => {
561 i += 1;
562 if i < rest.len() {
563 if let Ok(p) = rest[i].parse::<u16>() {
564 cfg.port = p;
565 }
566 }
567 }
568 arg if arg.starts_with("--port=") => {
569 if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
570 cfg.port = p;
571 }
572 }
573 "--project-root" => {
574 i += 1;
575 if i < rest.len() {
576 cfg.project_root = std::path::PathBuf::from(&rest[i]);
577 }
578 }
579 arg if arg.starts_with("--project-root=") => {
580 cfg.project_root =
581 std::path::PathBuf::from(&arg["--project-root=".len()..]);
582 }
583 "--auth-token" => {
584 i += 1;
585 if i < rest.len() {
586 cfg.auth_token = Some(rest[i].clone());
587 }
588 }
589 arg if arg.starts_with("--auth-token=") => {
590 cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
591 }
592 "--stateful" => cfg.stateful_mode = true,
593 "--stateless" => cfg.stateful_mode = false,
594 "--json" => cfg.json_response = true,
595 "--sse" => cfg.json_response = false,
596 "--disable-host-check" => cfg.disable_host_check = true,
597 "--allowed-host" => {
598 i += 1;
599 if i < rest.len() {
600 cfg.allowed_hosts.push(rest[i].clone());
601 }
602 }
603 arg if arg.starts_with("--allowed-host=") => {
604 cfg.allowed_hosts
605 .push(arg["--allowed-host=".len()..].to_string());
606 }
607 "--max-body-bytes" => {
608 i += 1;
609 if i < rest.len() {
610 if let Ok(n) = rest[i].parse::<usize>() {
611 cfg.max_body_bytes = n;
612 }
613 }
614 }
615 arg if arg.starts_with("--max-body-bytes=") => {
616 if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
617 cfg.max_body_bytes = n;
618 }
619 }
620 "--max-concurrency" => {
621 i += 1;
622 if i < rest.len() {
623 if let Ok(n) = rest[i].parse::<usize>() {
624 cfg.max_concurrency = n;
625 }
626 }
627 }
628 arg if arg.starts_with("--max-concurrency=") => {
629 if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
630 cfg.max_concurrency = n;
631 }
632 }
633 "--max-rps" => {
634 i += 1;
635 if i < rest.len() {
636 if let Ok(n) = rest[i].parse::<u32>() {
637 cfg.max_rps = n;
638 }
639 }
640 }
641 arg if arg.starts_with("--max-rps=") => {
642 if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
643 cfg.max_rps = n;
644 }
645 }
646 "--rate-burst" => {
647 i += 1;
648 if i < rest.len() {
649 if let Ok(n) = rest[i].parse::<u32>() {
650 cfg.rate_burst = n;
651 }
652 }
653 }
654 arg if arg.starts_with("--rate-burst=") => {
655 if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
656 cfg.rate_burst = n;
657 }
658 }
659 "--request-timeout-ms" => {
660 i += 1;
661 if i < rest.len() {
662 if let Ok(n) = rest[i].parse::<u64>() {
663 cfg.request_timeout_ms = n;
664 }
665 }
666 }
667 arg if arg.starts_with("--request-timeout-ms=") => {
668 if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
669 cfg.request_timeout_ms = n;
670 }
671 }
672 "--help" | "-h" => {
673 eprintln!(
674 "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
675 \\n\\
676 Options:\\n\\
677 --daemon, -d Start as background daemon (UDS)\\n\\
678 --stop Stop running daemon\\n\\
679 --status Show daemon status\\n\\
680 --host, -H Bind host (default: 127.0.0.1)\\n\\
681 --port, -p Bind port (default: 8080)\\n\\
682 --project-root Resolve relative paths against this root (default: cwd)\\n\\
683 --auth-token Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
684 --stateful/--stateless Streamable HTTP session mode (default: stateless)\\n\\
685 --json/--sse Response framing in stateless mode (default: json)\\n\\
686 --max-body-bytes Max request body size in bytes (default: 2097152)\\n\\
687 --max-concurrency Max concurrent requests (default: 32)\\n\\
688 --max-rps Max requests/sec (global, default: 50)\\n\\
689 --rate-burst Rate limiter burst (global, default: 100)\\n\\
690 --request-timeout-ms REST tool-call timeout (default: 30000)\\n\\
691 --allowed-host Add allowed Host header (repeatable)\\n\\
692 --disable-host-check Disable Host header validation (unsafe)"
693 );
694 return;
695 }
696 _ => {}
697 }
698 i += 1;
699 }
700
701 if stop_mode {
702 crate::daemon_autostart::stop();
703 if let Err(e) = crate::daemon::stop_daemon() {
704 eprintln!("Error: {e}");
705 std::process::exit(1);
706 }
707 return;
708 }
709
710 if status_mode {
711 println!("{}", crate::daemon::daemon_status());
712 return;
713 }
714
715 if daemon_mode {
716 if let Err(e) = crate::daemon::start_daemon(&rest) {
717 eprintln!("Error: {e}");
718 std::process::exit(1);
719 }
720 return;
721 }
722
723 if foreground_daemon {
724 if let Err(e) = crate::daemon::init_foreground_daemon() {
725 eprintln!("Error writing PID file: {e}");
726 std::process::exit(1);
727 }
728 let addr = crate::daemon::daemon_addr();
729 if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
730 {
731 tracing::error!("Daemon server error: {e}");
732 crate::daemon::cleanup_daemon_files();
733 std::process::exit(1);
734 }
735 crate::daemon::cleanup_daemon_files();
736 return;
737 }
738
739 if cfg.auth_token.is_none() {
740 if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
741 if !v.trim().is_empty() {
742 cfg.auth_token = Some(v);
743 }
744 }
745 }
746
747 if let Err(e) = run_async(crate::http_server::serve(cfg)) {
748 tracing::error!("HTTP server error: {e}");
749 std::process::exit(1);
750 }
751 return;
752 }
753 #[cfg(not(feature = "http-server"))]
754 {
755 eprintln!("lean-ctx serve is not available in this build");
756 std::process::exit(1);
757 }
758 }
759 "watch" => {
760 if rest.iter().any(|a| a == "--help" || a == "-h") {
761 println!("Usage: lean-ctx watch");
762 println!(" Live TUI dashboard (real-time event stream).");
763 return;
764 }
765 if let Err(e) = tui::run() {
766 tracing::error!("TUI error: {e}");
767 std::process::exit(1);
768 }
769 return;
770 }
771 "proxy" => {
772 #[cfg(feature = "http-server")]
773 {
774 let sub = rest.first().map_or("help", std::string::String::as_str);
775 match sub {
776 "start" => {
777 let port: u16 = rest
778 .iter()
779 .find_map(|p| {
780 p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
781 })
782 .and_then(|p| p.parse().ok())
783 .unwrap_or_else(crate::proxy_setup::default_port);
784 let autostart = rest.iter().any(|a| a == "--autostart");
785 if autostart {
786 crate::proxy_autostart::install(port, false);
787 return;
788 }
789 if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
790 tracing::error!("Proxy error: {e}");
791 std::process::exit(1);
792 }
793 }
794 "stop" => {
795 let port: u16 = rest
796 .iter()
797 .find_map(|p| p.strip_prefix("--port="))
798 .and_then(|p| p.parse().ok())
799 .unwrap_or_else(crate::proxy_setup::default_port);
800 let health_url = format!("http://127.0.0.1:{port}/health");
801 match ureq::get(&health_url).call() {
802 Ok(resp) => {
803 if let Ok(body) = resp.into_body().read_to_string() {
804 if let Some(pid_str) = body
805 .split("pid\":")
806 .nth(1)
807 .and_then(|s| s.split([',', '}']).next())
808 {
809 if let Ok(pid) = pid_str.trim().parse::<u32>() {
810 let _ =
811 crate::ipc::process::terminate_gracefully(pid);
812 std::thread::sleep(
813 std::time::Duration::from_millis(500),
814 );
815 if crate::ipc::process::is_alive(pid) {
816 let _ = crate::ipc::process::force_kill(pid);
817 }
818 println!(
819 "Proxy on port {port} stopped (PID {pid})."
820 );
821 return;
822 }
823 }
824 }
825 println!("Proxy on port {port} running but could not parse PID. Use `lean-ctx stop` to kill all.");
826 }
827 Err(_) => {
828 println!("No proxy running on port {port}.");
829 }
830 }
831 }
832 "status" => {
833 let port: u16 = rest
834 .iter()
835 .find_map(|p| p.strip_prefix("--port="))
836 .and_then(|p| p.parse().ok())
837 .unwrap_or_else(crate::proxy_setup::default_port);
838 let cfg = crate::core::config::Config::load();
839 println!("lean-ctx proxy:");
840 match cfg.proxy_enabled {
841 Some(true) => println!(" Config: enabled"),
842 Some(false) => println!(" Config: disabled"),
843 None => println!(" Config: undecided (not yet configured)"),
844 }
845 println!(" Port: {port}");
846 if let Ok(resp) =
847 ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
848 {
849 let body = resp.into_body().read_to_string().unwrap_or_default();
850 println!(" Process: running");
851 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
852 println!(" Requests: {}", v["requests_total"]);
853 println!(" Compressed: {}", v["requests_compressed"]);
854 println!(" Tokens saved: {}", v["tokens_saved"]);
855 println!(
856 " Compression: {}%",
857 v["compression_ratio_pct"].as_str().unwrap_or("0.0")
858 );
859 }
860 } else {
861 println!(" Process: not running");
862 }
863 if cfg.proxy_enabled == Some(false) || cfg.proxy_enabled.is_none() {
864 println!();
865 println!(" Enable: lean-ctx proxy enable");
866
867 let home = dirs::home_dir().unwrap_or_default();
868 if crate::proxy_setup::has_stale_proxy_url(&home) {
869 println!();
870 println!(" \x1b[33m⚠ WARNING: Claude Code ANTHROPIC_BASE_URL points to the local proxy,\x1b[0m");
871 println!(" \x1b[33m but proxy is not enabled. This causes 401 auth failures.\x1b[0m");
872 println!(" Fix: lean-ctx proxy cleanup (remove stale URL)");
873 println!(" lean-ctx proxy enable (enable the proxy)");
874 }
875 }
876 }
877 "enable" => {
878 let force = rest.iter().any(|a| a == "--force");
879 let mut cfg = crate::core::config::Config::load();
880 cfg.proxy_enabled = Some(true);
881 let _ = cfg.save();
882
883 let port = crate::proxy_setup::default_port();
884 crate::proxy_autostart::install(port, false);
885 std::thread::sleep(std::time::Duration::from_millis(500));
886
887 let home = dirs::home_dir().unwrap_or_default();
888 crate::proxy_setup::install_proxy_env_unchecked(
889 &home, port, false, force,
890 );
891 println!("\x1b[32m✓\x1b[0m Proxy enabled on port {port}. LLM requests will be compressed.");
892 }
893 "disable" => {
894 let mut cfg = crate::core::config::Config::load();
895 cfg.proxy_enabled = Some(false);
896 let _ = cfg.save();
897
898 crate::proxy_autostart::uninstall(false);
899 let home = dirs::home_dir().unwrap_or_default();
900 crate::proxy_setup::uninstall_proxy_env(&home, false);
901
902 println!(
903 "\x1b[32m✓\x1b[0m Proxy disabled. Original endpoint restored."
904 );
905 println!(" Re-enable anytime: lean-ctx proxy enable");
906 }
907 "cleanup" => {
908 let home = dirs::home_dir().unwrap_or_default();
909 let removed = crate::proxy_setup::cleanup_stale_proxy_env(&home);
910 if removed > 0 {
911 println!(
912 "\x1b[32m✓\x1b[0m Cleaned up {removed} stale proxy URL(s)."
913 );
914 println!(" Restart your AI tool for changes to take effect.");
915 } else {
916 println!(" No stale proxy URLs found. Nothing to clean up.");
917 }
918 }
919 _ => {
920 println!("Usage: lean-ctx proxy <start|stop|status|enable|disable|cleanup> [--port=4444]");
921 }
922 }
923 return;
924 }
925 #[cfg(not(feature = "http-server"))]
926 {
927 eprintln!("lean-ctx proxy is not available in this build");
928 std::process::exit(1);
929 }
930 }
931 "daemon" => {
932 let sub = rest.first().map_or("status", std::string::String::as_str);
933 match sub {
934 "enable" => {
935 crate::daemon_autostart::install(false);
936 println!(
937 "\x1b[32m✓\x1b[0m Daemon autostart enabled. Will start on login and restart if stopped."
938 );
939 }
940 "disable" => {
941 crate::daemon_autostart::uninstall(false);
942 println!("\x1b[32m✓\x1b[0m Daemon autostart disabled.");
943 }
944 "start" => {
945 if let Err(e) = crate::daemon::start_daemon(&rest[1..]) {
946 eprintln!("Error: {e}");
947 std::process::exit(1);
948 }
949 }
950 "stop" => {
951 crate::daemon_autostart::stop();
952 match crate::daemon::stop_daemon() {
953 Ok(()) => println!("Daemon stopped."),
954 Err(e) => eprintln!("Error: {e}"),
955 }
956 }
957 "status" => {
958 if crate::daemon::is_daemon_running() {
959 let pid = crate::daemon::read_daemon_pid().unwrap_or(0);
960 println!("lean-ctx daemon:");
961 println!(" Status: running (PID {pid})");
962 println!(
963 " Autostart: {}",
964 if crate::daemon_autostart::is_installed() {
965 "enabled"
966 } else {
967 "not installed (run: lean-ctx daemon enable)"
968 }
969 );
970 } else {
971 println!("lean-ctx daemon:");
972 println!(" Status: not running");
973 println!(
974 " Autostart: {}",
975 if crate::daemon_autostart::is_installed() {
976 "enabled"
977 } else {
978 "not installed"
979 }
980 );
981 println!();
982 println!(" Start: lean-ctx daemon start");
983 println!(" Autostart: lean-ctx daemon enable");
984 }
985 }
986 _ => {
987 println!("Usage: lean-ctx daemon <start|stop|status|enable|disable>");
988 }
989 }
990 return;
991 }
992 "init" => {
993 super::cmd_init(&rest);
994 return;
995 }
996 "setup" => {
997 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
998 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
999 let fix = rest.iter().any(|a| a == "--fix");
1000 let json = rest.iter().any(|a| a == "--json");
1001 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
1002
1003 if non_interactive || fix || json || yes {
1004 let opts = setup::SetupOptions {
1005 non_interactive,
1006 yes,
1007 fix,
1008 json,
1009 no_auto_approve,
1010 ..Default::default()
1011 };
1012 match setup::run_setup_with_options(opts) {
1013 Ok(report) => {
1014 if json {
1015 println!(
1016 "{}",
1017 serde_json::to_string_pretty(&report)
1018 .unwrap_or_else(|_| "{}".to_string())
1019 );
1020 }
1021 if !report.success {
1022 std::process::exit(1);
1023 }
1024 }
1025 Err(e) => {
1026 eprintln!("{e}");
1027 std::process::exit(1);
1028 }
1029 }
1030 } else {
1031 setup::run_setup();
1032 }
1033 return;
1034 }
1035 "install" => {
1036 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
1037 let json = rest.iter().any(|a| a == "--json");
1038 if !repair {
1039 eprintln!("Usage: lean-ctx install --repair [--json]");
1040 std::process::exit(1);
1041 }
1042 let opts = setup::SetupOptions {
1043 non_interactive: true,
1044 yes: true,
1045 fix: true,
1046 json,
1047 ..Default::default()
1048 };
1049 match setup::run_setup_with_options(opts) {
1050 Ok(report) => {
1051 if json {
1052 println!(
1053 "{}",
1054 serde_json::to_string_pretty(&report)
1055 .unwrap_or_else(|_| "{}".to_string())
1056 );
1057 }
1058 if !report.success {
1059 std::process::exit(1);
1060 }
1061 }
1062 Err(e) => {
1063 eprintln!("{e}");
1064 std::process::exit(1);
1065 }
1066 }
1067 return;
1068 }
1069 "bootstrap" => {
1070 let json = rest.iter().any(|a| a == "--json");
1071 let opts = setup::SetupOptions {
1072 non_interactive: true,
1073 yes: true,
1074 fix: true,
1075 json,
1076 ..Default::default()
1077 };
1078 match setup::run_setup_with_options(opts) {
1079 Ok(report) => {
1080 if json {
1081 println!(
1082 "{}",
1083 serde_json::to_string_pretty(&report)
1084 .unwrap_or_else(|_| "{}".to_string())
1085 );
1086 }
1087 if !report.success {
1088 std::process::exit(1);
1089 }
1090 }
1091 Err(e) => {
1092 eprintln!("{e}");
1093 std::process::exit(1);
1094 }
1095 }
1096 return;
1097 }
1098 "status" => {
1099 let code = status::run_cli(&rest);
1100 if code != 0 {
1101 std::process::exit(code);
1102 }
1103 return;
1104 }
1105 "read" => {
1106 super::cmd_read(&rest);
1107 core::stats::flush();
1108 return;
1109 }
1110 "diff" => {
1111 super::cmd_diff(&rest);
1112 core::stats::flush();
1113 return;
1114 }
1115 "grep" => {
1116 super::cmd_grep(&rest);
1117 core::stats::flush();
1118 return;
1119 }
1120 "find" => {
1121 super::cmd_find(&rest);
1122 core::stats::flush();
1123 return;
1124 }
1125 "ls" => {
1126 super::cmd_ls(&rest);
1127 core::stats::flush();
1128 return;
1129 }
1130 "deps" => {
1131 super::cmd_deps(&rest);
1132 core::stats::flush();
1133 return;
1134 }
1135 "discover" => {
1136 super::cmd_discover(&rest);
1137 return;
1138 }
1139 "ghost" => {
1140 super::cmd_ghost(&rest);
1141 return;
1142 }
1143 "filter" => {
1144 super::cmd_filter(&rest);
1145 return;
1146 }
1147 "heatmap" => {
1148 heatmap::cmd_heatmap(&rest);
1149 return;
1150 }
1151 "graph" => {
1152 let sub = rest.first().map_or("build", std::string::String::as_str);
1153 match sub {
1154 "build" => {
1155 let root = rest.get(1).cloned().or_else(|| {
1156 std::env::current_dir()
1157 .ok()
1158 .map(|p| p.to_string_lossy().to_string())
1159 });
1160 let root = root.unwrap_or_else(|| ".".to_string());
1161 let index = core::graph_index::load_or_build(&root);
1162 println!(
1163 "Graph built: {} files, {} edges",
1164 index.files.len(),
1165 index.edges.len()
1166 );
1167 }
1168 "export-html" => {
1169 let mut root: Option<String> = None;
1170 let mut out: Option<String> = None;
1171 let mut max_nodes: usize = 2500;
1172
1173 let args = &rest[1..];
1174 let mut i = 0usize;
1175 while i < args.len() {
1176 let a = args[i].as_str();
1177 if let Some(v) = a.strip_prefix("--root=") {
1178 root = Some(v.to_string());
1179 } else if a == "--root" {
1180 root = args.get(i + 1).cloned();
1181 i += 1;
1182 } else if let Some(v) = a.strip_prefix("--out=") {
1183 out = Some(v.to_string());
1184 } else if a == "--out" {
1185 out = args.get(i + 1).cloned();
1186 i += 1;
1187 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1188 max_nodes = v.parse::<usize>().unwrap_or(0);
1189 } else if a == "--max-nodes" {
1190 let v = args.get(i + 1).map_or("", String::as_str);
1191 max_nodes = v.parse::<usize>().unwrap_or(0);
1192 i += 1;
1193 }
1194 i += 1;
1195 }
1196
1197 let root = root
1198 .or_else(|| {
1199 std::env::current_dir()
1200 .ok()
1201 .map(|p| p.to_string_lossy().to_string())
1202 })
1203 .unwrap_or_else(|| ".".to_string());
1204 let Some(out) = out else {
1205 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1206 std::process::exit(1);
1207 };
1208 if max_nodes == 0 {
1209 eprintln!("--max-nodes must be >= 1");
1210 std::process::exit(1);
1211 }
1212
1213 core::graph_export::export_graph_html(
1214 &root,
1215 std::path::Path::new(&out),
1216 max_nodes,
1217 )
1218 .unwrap_or_else(|e| {
1219 eprintln!("graph export failed: {e}");
1220 std::process::exit(1);
1221 });
1222 println!("{out}");
1223 }
1224 "related" | "impact" | "symbol" | "context" | "status" => {
1225 let path_arg = if sub == "status" {
1226 None
1227 } else {
1228 rest.get(1).map(String::as_str)
1229 };
1230 let root_idx = if sub == "status" { 1 } else { 2 };
1231 let root = resolve_graph_root(rest.get(root_idx));
1232 println!(
1233 "{}",
1234 tools::ctx_graph::handle(
1235 sub,
1236 path_arg,
1237 &root,
1238 &mut core::cache::SessionCache::new(),
1239 tools::CrpMode::Off,
1240 None,
1241 None,
1242 )
1243 );
1244 }
1245 _ => {
1246 eprintln!(
1247 "Usage:\n \
1248 lean-ctx graph build [path]\n \
1249 lean-ctx graph related <file>\n \
1250 lean-ctx graph impact <file|symbol>\n \
1251 lean-ctx graph symbol <name>\n \
1252 lean-ctx graph context <query>\n \
1253 lean-ctx graph status\n \
1254 lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1255 );
1256 std::process::exit(1);
1257 }
1258 }
1259 return;
1260 }
1261 "smells" => {
1262 let action = rest.first().map_or("summary", String::as_str);
1263 let rule = rest.iter().enumerate().find_map(|(i, a)| {
1264 if let Some(v) = a.strip_prefix("--rule=") {
1265 return Some(v.to_string());
1266 }
1267 if a == "--rule" {
1268 return rest.get(i + 1).cloned();
1269 }
1270 None
1271 });
1272 let path = rest.iter().enumerate().find_map(|(i, a)| {
1273 if let Some(v) = a.strip_prefix("--path=") {
1274 return Some(v.to_string());
1275 }
1276 if a == "--path" {
1277 return rest.get(i + 1).cloned();
1278 }
1279 None
1280 });
1281 let root = rest
1282 .iter()
1283 .enumerate()
1284 .find_map(|(i, a)| {
1285 if let Some(v) = a.strip_prefix("--root=") {
1286 return Some(v.to_string());
1287 }
1288 if a == "--root" {
1289 return rest.get(i + 1).cloned();
1290 }
1291 None
1292 })
1293 .or_else(|| {
1294 std::env::current_dir()
1295 .ok()
1296 .map(|p| p.to_string_lossy().to_string())
1297 })
1298 .unwrap_or_else(|| ".".to_string());
1299 let fmt = if rest.iter().any(|a| a == "--json") {
1300 Some("json")
1301 } else {
1302 None
1303 };
1304 println!(
1305 "{}",
1306 tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1307 );
1308 return;
1309 }
1310 "session" => {
1311 super::cmd_session_action(&rest);
1312 return;
1313 }
1314 "ledger" => {
1315 super::cmd_ledger(&rest);
1316 return;
1317 }
1318 "control" | "context-control" => {
1319 super::cmd_control(&rest);
1320 return;
1321 }
1322 "plan" | "context-plan" => {
1323 super::cmd_plan(&rest);
1324 return;
1325 }
1326 "compile" | "context-compile" => {
1327 super::cmd_compile(&rest);
1328 return;
1329 }
1330 "knowledge" => {
1331 super::cmd_knowledge(&rest);
1332 return;
1333 }
1334 "overview" => {
1335 super::cmd_overview(&rest);
1336 return;
1337 }
1338 "compress" => {
1339 super::cmd_compress(&rest);
1340 return;
1341 }
1342 "wrapped" => {
1343 super::cmd_wrapped(&rest);
1344 return;
1345 }
1346 "sessions" => {
1347 super::cmd_sessions(&rest);
1348 return;
1349 }
1350 "benchmark" => {
1351 super::cmd_benchmark(&rest);
1352 return;
1353 }
1354 "profile" => {
1355 super::cmd_profile(&rest);
1356 return;
1357 }
1358 "config" => {
1359 super::cmd_config(&rest);
1360 return;
1361 }
1362 "stats" => {
1363 super::cmd_stats(&rest);
1364 return;
1365 }
1366 "cache" => {
1367 super::cmd_cache(&rest);
1368 return;
1369 }
1370 "theme" => {
1371 super::cmd_theme(&rest);
1372 return;
1373 }
1374 "tee" => {
1375 super::cmd_tee(&rest);
1376 return;
1377 }
1378 "terse" | "compression" => {
1379 super::cmd_compression(&rest);
1380 return;
1381 }
1382 "slow-log" => {
1383 super::cmd_slow_log(&rest);
1384 return;
1385 }
1386 "update" | "--self-update" => {
1387 core::updater::run(&rest);
1388 return;
1389 }
1390 "restart" => {
1391 cmd_restart();
1392 return;
1393 }
1394 "stop" => {
1395 cmd_stop();
1396 return;
1397 }
1398 "dev-install" => {
1399 cmd_dev_install();
1400 return;
1401 }
1402 "doctor" => {
1403 let code = doctor::run_cli(&rest);
1404 if code != 0 {
1405 std::process::exit(code);
1406 }
1407 return;
1408 }
1409 "harden" => {
1410 super::harden::run(&rest);
1411 return;
1412 }
1413 "export-rules" => {
1414 super::export_rules::run(&rest);
1415 return;
1416 }
1417 "gotchas" | "bugs" => {
1418 super::cloud::cmd_gotchas(&rest);
1419 return;
1420 }
1421 "learn" => {
1422 super::cmd_learn(&rest);
1423 return;
1424 }
1425 "buddy" | "pet" => {
1426 super::cloud::cmd_buddy(&rest);
1427 return;
1428 }
1429 "hook" => {
1430 hook_handlers::mark_hook_environment();
1431 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1432 let action = rest.first().map_or("help", std::string::String::as_str);
1433 match action {
1434 "rewrite" => hook_handlers::handle_rewrite(),
1435 "redirect" => hook_handlers::handle_redirect(),
1436 "observe" => hook_handlers::handle_observe(),
1437 "copilot" => hook_handlers::handle_copilot(),
1438 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1439 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1440 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1441 _ => {
1442 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1443 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1444 std::process::exit(1);
1445 }
1446 }
1447 return;
1448 }
1449 "report-issue" | "report" => {
1450 report::run(&rest);
1451 return;
1452 }
1453 "uninstall" => {
1454 let dry_run = rest.iter().any(|a| a == "--dry-run");
1455 let keep_config = rest.iter().any(|a| a == "--keep-config");
1456 uninstall::run(dry_run, keep_config);
1457 return;
1458 }
1459 "bypass" => {
1460 if rest.is_empty() {
1461 eprintln!("Usage: lean-ctx bypass \"command\"");
1462 eprintln!("Runs the command with zero compression (raw passthrough).");
1463 std::process::exit(1);
1464 }
1465 let command = if rest.len() == 1 {
1466 rest[0].clone()
1467 } else {
1468 shell::join_command(&args[2..])
1469 };
1470 std::env::set_var("LEAN_CTX_RAW", "1");
1471 let code = shell::exec(&command);
1472 std::process::exit(code);
1473 }
1474 "safety-levels" | "safety" => {
1475 println!("{}", core::compression_safety::format_safety_table());
1476 return;
1477 }
1478 "cheat" | "cheatsheet" | "cheat-sheet" => {
1479 super::cmd_cheatsheet();
1480 return;
1481 }
1482 "login" => {
1483 super::cloud::cmd_login(&rest);
1484 return;
1485 }
1486 "register" => {
1487 super::cloud::cmd_register(&rest);
1488 return;
1489 }
1490 "forgot-password" => {
1491 super::cloud::cmd_forgot_password(&rest);
1492 return;
1493 }
1494 "sync" => {
1495 super::cloud::cmd_sync();
1496 return;
1497 }
1498 "contribute" => {
1499 super::cloud::cmd_contribute();
1500 return;
1501 }
1502 "cloud" => {
1503 super::cloud::cmd_cloud(&rest);
1504 return;
1505 }
1506 "upgrade" => {
1507 super::cloud::cmd_upgrade();
1508 return;
1509 }
1510 "--version" | "-V" => {
1511 println!("{}", core::integrity::origin_line());
1512 return;
1513 }
1514 "--help" | "-h" => {
1515 print_help();
1516 return;
1517 }
1518 "mcp" => {}
1519 _ => {
1520 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1521 print_help();
1522 std::process::exit(1);
1523 }
1524 }
1525 }
1526
1527 if let Err(e) = run_mcp_server() {
1528 tracing::error!("lean-ctx: {e}");
1529 std::process::exit(1);
1530 }
1531}
1532
1533fn resolve_graph_root(arg: Option<&String>) -> String {
1534 arg.cloned()
1535 .or_else(|| {
1536 std::env::current_dir()
1537 .ok()
1538 .map(|p| p.to_string_lossy().to_string())
1539 })
1540 .unwrap_or_else(|| ".".to_string())
1541}
1542
1543fn passthrough(command: &str) -> ! {
1544 let (shell, flag) = shell::shell_and_flag();
1545 let mut cmd = std::process::Command::new(&shell);
1546 cmd.arg(&flag).arg(command).env("LEAN_CTX_ACTIVE", "1");
1547 shell::platform::apply_utf8_locale(&mut cmd);
1548 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
1549 std::process::exit(status);
1550}
1551
1552fn run_async<F: std::future::Future>(future: F) -> F::Output {
1553 tokio::runtime::Runtime::new()
1554 .expect("failed to create async runtime")
1555 .block_on(future)
1556}
1557
1558fn run_mcp_server() -> Result<()> {
1559 use rmcp::ServiceExt;
1560
1561 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1562
1563 crate::core::startup_guard::crash_loop_backoff(crate::core::startup_guard::MCP_PROCESS_NAME);
1564
1565 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1571 "mcp-startup",
1572 std::time::Duration::from_secs(3),
1573 std::time::Duration::from_secs(30),
1574 );
1575
1576 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1577 let worker_threads = resolve_worker_threads(parallelism);
1578 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1579
1580 let rt = tokio::runtime::Builder::new_multi_thread()
1581 .worker_threads(worker_threads)
1582 .max_blocking_threads(max_blocking_threads)
1583 .enable_all()
1584 .build()?;
1585
1586 let server = tools::create_server();
1587 drop(startup_lock);
1588
1589 spawn_proxy_if_needed();
1591
1592 rt.block_on(async {
1593 core::logging::init_mcp_logging();
1594 core::protocol::set_mcp_context(true);
1595
1596 tracing::info!(
1597 "lean-ctx v{} MCP server starting",
1598 env!("CARGO_PKG_VERSION")
1599 );
1600
1601 let transport =
1602 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1603 let server_handle = server.clone();
1604 let service = match server.serve(transport).await {
1605 Ok(s) => s,
1606 Err(e) => {
1607 let msg = e.to_string();
1608 if msg.contains("expect initialized")
1609 || msg.contains("context canceled")
1610 || msg.contains("broken pipe")
1611 {
1612 tracing::debug!("Client disconnected before init: {msg}");
1613 return Ok(());
1614 }
1615 return Err(e.into());
1616 }
1617 };
1618 match service.waiting().await {
1619 Ok(reason) => {
1620 tracing::info!("MCP server stopped: {reason:?}");
1621 }
1622 Err(e) => {
1623 let msg = e.to_string();
1624 if msg.contains("broken pipe")
1625 || msg.contains("connection reset")
1626 || msg.contains("context canceled")
1627 {
1628 tracing::info!("MCP server: transport closed ({msg})");
1629 } else {
1630 tracing::error!("MCP server error: {msg}");
1631 }
1632 }
1633 }
1634
1635 server_handle.shutdown().await;
1636
1637 core::stats::flush();
1638 core::heatmap::flush();
1639 core::mode_predictor::ModePredictor::flush();
1640 core::feedback::FeedbackStore::flush();
1641
1642 Ok(())
1643 })
1644}
1645
1646fn print_help() {
1647 println!(
1648 "lean-ctx {version} — Context Runtime for AI Agents
1649
165060+ compression patterns | 61 MCP tools | 10 read modes | Context Continuity Protocol
1651
1652USAGE:
1653 lean-ctx Start MCP server (stdio)
1654 lean-ctx serve Start MCP server (Streamable HTTP)
1655 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1656 lean-ctx serve --stop Stop running daemon
1657 lean-ctx serve --status Show daemon status
1658 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1659 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1660 lean-ctx -c --raw \"command\" Execute without compression (full output)
1661 lean-ctx exec \"command\" Same as -c
1662 lean-ctx shell Interactive shell with compression
1663
1664COMMANDS:
1665 gain Visual dashboard (colors, bars, sparklines, USD)
1666 gain --live Live mode: auto-refreshes every 1s in-place
1667 gain --graph 30-day savings chart
1668 gain --daily Bordered day-by-day table with USD
1669 gain --json Raw JSON export of all stats
1670 token-report [--json] Token + memory report (project + session + CEP)
1671 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1672 index <status|build|build-full|watch> Codebase index utilities
1673 cep CEP impact report (score trends, cache, modes)
1674 watch Live TUI dashboard (real-time event stream)
1675 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1676 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1677 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1678 proxy status Show proxy statistics
1679 daemon start|stop|status IPC daemon management
1680 daemon enable|disable Auto-start daemon on login (systemd/LaunchAgent)
1681 cache [list|clear|stats] Show/manage file read cache
1682 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1683 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1684 benchmark run [path] [--json] Run real benchmark on project files
1685 benchmark report [path] Generate shareable Markdown report
1686 cheatsheet Command cheat sheet & workflow quick reference
1687 setup One-command setup: shell + editor + verify
1688 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1689 bootstrap Non-interactive setup + fix (zero-config)
1690 status [--json] Show setup + MCP + rules status
1691 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1692 init --agent <name> Configure MCP for specific editor/agent
1693 read <file> [-m mode] Read file with compression
1694 diff <file1> <file2> Compressed file diff
1695 grep <pattern> [path] Search with compressed output
1696 find <pattern> [path] Find files with compressed output
1697 ls [path] Directory listing with compression
1698 deps [path] Show project dependencies
1699 discover Find uncompressed commands in shell history
1700 ghost [--json] Ghost Token report: find hidden token waste
1701 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1702 session Show adoption statistics
1703 session task <desc> Set current task
1704 session finding <summary> Record a finding
1705 session save Save current session
1706 session load [id] Load session (latest if no ID)
1707 knowledge remember <value> --category <c> --key <k> Store a fact
1708 knowledge recall [query] [--category <c>] Retrieve facts
1709 knowledge search <query> Cross-project knowledge search
1710 knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
1711 knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
1712 knowledge remove --category <c> --key <k> Remove a fact
1713 knowledge status Knowledge base summary
1714 overview [task] Project overview (task-contextualized if given)
1715 compress [--signatures] Context compression checkpoint
1716 config Show/edit configuration (~/.lean-ctx/config.toml)
1717 profile [list|show|diff|create|set] Manage context profiles
1718 theme [list|set|export|import] Customize terminal colors and themes
1719 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1720 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1721 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1722 update [--check] Self-update lean-ctx binary from GitHub Releases
1723 stop Stop ALL lean-ctx processes (daemon, proxy, orphans)
1724 restart Restart daemon (applies config.toml changes)
1725 dev-install Build release + atomic install + restart (for development)
1726 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1727 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1728 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1729 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1730 smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1731 Code smell detection (Property Graph, 8 rules)
1732 control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1733 plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
1734 compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1735 uninstall [--keep-config] Remove all lean-ctx artifacts (--keep-config preserves MCP/rules)
1736
1737SHELL HOOK PATTERNS (95+):
1738 git status, log, diff, add, commit, push, pull, fetch, clone,
1739 branch, checkout, switch, merge, stash, tag, reset, remote
1740 docker build, ps, images, logs, compose, exec, network
1741 npm/pnpm install, test, run, list, outdated, audit
1742 cargo build, test, check, clippy
1743 gh pr list/view/create, issue list/view, run list/view
1744 kubectl get pods/services/deployments, logs, describe, apply
1745 python pip install/list/outdated, ruff check/format, poetry, uv
1746 linters eslint, biome, prettier, golangci-lint
1747 builds tsc, next build, vite build
1748 ruby rubocop, bundle install/update, rake test, rails test
1749 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1750 iac terraform, make, maven, gradle, dotnet, flutter, dart
1751 utils curl, grep/rg, find, ls, wget, env
1752 data JSON schema extraction, log deduplication
1753
1754READ MODES:
1755 auto Auto-select optimal mode (default)
1756 full Full content (cached re-reads = 13 tokens)
1757 map Dependency graph + API signatures
1758 signatures tree-sitter AST extraction (18 languages)
1759 task Task-relevant filtering (requires ctx_session task)
1760 reference One-line reference stub (cheap cache key)
1761 aggressive Syntax-stripped content
1762 entropy Shannon entropy filtered
1763 diff Changed lines only
1764 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1765
1766ENVIRONMENT:
1767 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1768 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1769 LEAN_CTX_RAW=1 Same as --raw for current command
1770 LEAN_CTX_AUTONOMY=false Disable autonomous features
1771 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1772
1773OPTIONS:
1774 --version, -V Show version
1775 --help, -h Show this help
1776
1777EXAMPLES:
1778 lean-ctx -c \"git status\" Compressed git output
1779 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1780 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1781 lean-ctx gain Visual terminal dashboard
1782 lean-ctx gain --live Live auto-updating terminal dashboard
1783 lean-ctx gain --graph 30-day savings chart
1784 lean-ctx gain --daily Day-by-day breakdown with USD
1785 lean-ctx token-report --json Machine-readable token + memory report
1786 lean-ctx dashboard Open web dashboard at localhost:3333
1787 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1788 lean-ctx gain --wrapped Wrapped report card (recommended)
1789 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1790 lean-ctx sessions list List all CCP sessions
1791 lean-ctx sessions show Show latest session state
1792 lean-ctx discover Find missed savings in shell history
1793 lean-ctx setup One-command setup (shell + editors + verify)
1794 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1795 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1796 lean-ctx bootstrap --json Machine-readable bootstrap report
1797 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1798 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1799 lean-ctx-off Disable all shell aliases
1800 lean-ctx-mode track Track mode: full output, stats recorded (default)
1801 lean-ctx-mode compress Compress mode: all output compressed (power users)
1802 lean-ctx-mode off Same as lean-ctx-off
1803 lean-ctx-status Show whether compression is active
1804 lean-ctx init --agent pi Install Pi Coding Agent extension
1805 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1806 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1807 lean-ctx doctor --fix --json Repair + machine-readable report
1808 lean-ctx status --json Machine-readable current status
1809 lean-ctx session task \"implement auth\"
1810 lean-ctx session finding \"auth.rs:42 — missing validation\"
1811 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1812 lean-ctx knowledge recall \"authentication\"
1813 lean-ctx knowledge search \"database migration\"
1814 lean-ctx overview \"refactor auth module\"
1815 lean-ctx compress --signatures
1816 lean-ctx read src/main.rs -m map
1817 lean-ctx grep \"pub fn\" src/
1818 lean-ctx deps .
1819
1820GRAPH (project analysis):
1821 lean-ctx graph build [path] Build/rebuild project graph index
1822 lean-ctx graph status Show graph index statistics
1823 lean-ctx graph related <file> List files related to a given file
1824 lean-ctx graph impact <file> Show files impacted by changes to a file
1825 lean-ctx graph symbol <spec> Inspect a symbol (format: file.rs::fn_name)
1826 lean-ctx graph context <query> Query the property graph for a concept
1827
1828CLOUD:
1829 cloud status Show cloud connection status
1830 login <email> Log into existing LeanCTX Cloud account
1831 register <email> Create a new LeanCTX Cloud account
1832 forgot-password <email> Send password reset email
1833 sync Upload local stats to cloud dashboard
1834 contribute Share anonymized compression data
1835
1836TROUBLESHOOTING:
1837 Commands broken? lean-ctx-off (fixes current session)
1838 Permanent fix? lean-ctx uninstall (removes all hooks)
1839 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1840 Binary missing? Aliases auto-fallback to original commands (safe)
1841 Preview init? lean-ctx init --global --dry-run
1842
1843WEBSITE: https://leanctx.com
1844GITHUB: https://github.com/yvgude/lean-ctx
1845",
1846 version = env!("CARGO_PKG_VERSION"),
1847 );
1848}
1849
1850fn cmd_stop() {
1851 use crate::daemon;
1852 use crate::ipc;
1853
1854 eprintln!("Stopping all lean-ctx processes…");
1855
1856 crate::proxy_autostart::stop();
1857 crate::daemon_autostart::stop();
1858 eprintln!(" Unloaded autostart (LaunchAgent/systemd).");
1859
1860 if let Err(e) = daemon::stop_daemon() {
1862 eprintln!(" Warning: daemon stop: {e}");
1863 }
1864
1865 let killed = ipc::process::kill_all_by_name("lean-ctx");
1867 if killed > 0 {
1868 eprintln!(" Sent SIGTERM to {killed} process(es).");
1869 }
1870
1871 std::thread::sleep(std::time::Duration::from_millis(500));
1872
1873 let remaining = ipc::process::find_killable_pids("lean-ctx");
1875 if !remaining.is_empty() {
1876 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
1877 for &pid in &remaining {
1878 let _ = ipc::process::force_kill(pid);
1879 }
1880 std::thread::sleep(std::time::Duration::from_millis(300));
1881 }
1882
1883 daemon::cleanup_daemon_files();
1884
1885 let final_check = ipc::process::find_killable_pids("lean-ctx");
1886 if final_check.is_empty() {
1887 eprintln!(" ✓ All lean-ctx processes stopped.");
1888 } else {
1889 eprintln!(
1890 " ✗ {} process(es) could not be killed: {:?}",
1891 final_check.len(),
1892 final_check
1893 );
1894 eprintln!(
1895 " Try: sudo kill -9 {}",
1896 final_check
1897 .iter()
1898 .map(std::string::ToString::to_string)
1899 .collect::<Vec<_>>()
1900 .join(" ")
1901 );
1902 std::process::exit(1);
1903 }
1904}
1905
1906fn cmd_restart() {
1907 use crate::daemon;
1908 use crate::ipc;
1909
1910 eprintln!("Restarting lean-ctx…");
1911
1912 crate::proxy_autostart::stop();
1913 crate::daemon_autostart::stop();
1914
1915 if let Err(e) = daemon::stop_daemon() {
1916 eprintln!(" Warning: daemon stop: {e}");
1917 }
1918
1919 let orphans = ipc::process::kill_all_by_name("lean-ctx");
1920 if orphans > 0 {
1921 eprintln!(" Terminated {orphans} orphan process(es).");
1922 }
1923
1924 std::thread::sleep(std::time::Duration::from_millis(500));
1925
1926 let remaining = ipc::process::find_pids_by_name("lean-ctx");
1927 if !remaining.is_empty() {
1928 eprintln!(
1929 " Force-killing {} stubborn process(es): {:?}",
1930 remaining.len(),
1931 remaining
1932 );
1933 for &pid in &remaining {
1934 let _ = ipc::process::force_kill(pid);
1935 }
1936 std::thread::sleep(std::time::Duration::from_millis(300));
1937 }
1938
1939 daemon::cleanup_daemon_files();
1940
1941 crate::proxy_autostart::start();
1942
1943 if crate::daemon_autostart::is_installed() {
1944 crate::daemon_autostart::start();
1945 eprintln!(" ✓ Daemon restarted via autostart.");
1946 } else {
1947 match daemon::start_daemon(&[]) {
1948 Ok(()) => eprintln!(" ✓ Daemon restarted."),
1949 Err(e) => {
1950 eprintln!(" ✗ Daemon start failed: {e}");
1951 std::process::exit(1);
1952 }
1953 }
1954 }
1955}
1956
1957fn cmd_dev_install() {
1958 use crate::ipc;
1959
1960 let cargo_root = find_cargo_project_root();
1961 let Some(cargo_root) = cargo_root else {
1962 eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1963 std::process::exit(1);
1964 };
1965
1966 eprintln!("Building release binary…");
1967 let build = std::process::Command::new("cargo")
1968 .args(["build", "--release"])
1969 .current_dir(&cargo_root)
1970 .status();
1971
1972 match build {
1973 Ok(s) if s.success() => {}
1974 Ok(s) => {
1975 eprintln!(" Build failed with exit code {}", s.code().unwrap_or(-1));
1976 std::process::exit(1);
1977 }
1978 Err(e) => {
1979 eprintln!(" Build failed: {e}");
1980 std::process::exit(1);
1981 }
1982 }
1983
1984 let built_binary = cargo_root.join("target/release/lean-ctx");
1985 if !built_binary.exists() {
1986 eprintln!(
1987 " Error: Built binary not found at {}",
1988 built_binary.display()
1989 );
1990 std::process::exit(1);
1991 }
1992
1993 let install_path = resolve_install_path();
1994 eprintln!("Installing to {}…", install_path.display());
1995
1996 eprintln!(" Stopping all lean-ctx processes…");
1997 crate::proxy_autostart::stop();
1998 crate::daemon_autostart::stop();
1999 let _ = crate::daemon::stop_daemon();
2000 ipc::process::kill_all_by_name("lean-ctx");
2001 std::thread::sleep(std::time::Duration::from_millis(500));
2002
2003 let remaining = ipc::process::find_pids_by_name("lean-ctx");
2004 if !remaining.is_empty() {
2005 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
2006 for &pid in &remaining {
2007 let _ = ipc::process::force_kill(pid);
2008 }
2009 std::thread::sleep(std::time::Duration::from_millis(500));
2010 }
2011
2012 let old_path = install_path.with_extension("old");
2013 if install_path.exists() {
2014 if let Err(e) = std::fs::rename(&install_path, &old_path) {
2015 eprintln!(" Warning: rename existing binary: {e}");
2016 }
2017 }
2018
2019 match std::fs::copy(&built_binary, &install_path) {
2020 Ok(_) => {
2021 let _ = std::fs::remove_file(&old_path);
2022 #[cfg(unix)]
2023 {
2024 use std::os::unix::fs::PermissionsExt;
2025 let _ =
2026 std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
2027 }
2028 eprintln!(" ✓ Binary installed.");
2029 }
2030 Err(e) => {
2031 eprintln!(" Error: copy failed: {e}");
2032 if old_path.exists() {
2033 let _ = std::fs::rename(&old_path, &install_path);
2034 eprintln!(" Rolled back to previous binary.");
2035 }
2036 std::process::exit(1);
2037 }
2038 }
2039
2040 let version = std::process::Command::new(&install_path)
2041 .arg("--version")
2042 .output()
2043 .map_or_else(
2044 |_| "unknown".to_string(),
2045 |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
2046 );
2047
2048 eprintln!(" ✓ dev-install complete: {version}");
2049
2050 eprintln!(" Re-enabling autostart…");
2051 crate::proxy_autostart::start();
2052
2053 if crate::daemon_autostart::is_installed() {
2054 crate::daemon_autostart::start();
2055 eprintln!(" ✓ Daemon restarted via autostart.");
2056 } else {
2057 eprintln!(" Starting daemon…");
2058 match crate::daemon::start_daemon(&[]) {
2059 Ok(()) => {}
2060 Err(e) => eprintln!(" Warning: daemon start: {e} (will be started by editor)"),
2061 }
2062 }
2063}
2064
2065fn find_cargo_project_root() -> Option<std::path::PathBuf> {
2066 let mut dir = std::env::current_dir().ok()?;
2067 loop {
2068 if dir.join("Cargo.toml").exists() {
2069 return Some(dir);
2070 }
2071 if !dir.pop() {
2072 return None;
2073 }
2074 }
2075}
2076
2077fn resolve_install_path() -> std::path::PathBuf {
2078 if let Ok(exe) = std::env::current_exe() {
2079 if let Ok(canonical) = exe.canonicalize() {
2080 let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
2081 if !is_in_cargo_target && canonical.exists() {
2082 return canonical;
2083 }
2084 }
2085 }
2086
2087 if let Ok(home) = std::env::var("HOME") {
2088 let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
2089 if local_bin.parent().is_some_and(std::path::Path::exists) {
2090 return local_bin;
2091 }
2092 }
2093
2094 std::path::PathBuf::from("/usr/local/bin/lean-ctx")
2095}
2096
2097fn spawn_proxy_if_needed() {
2098 use std::net::TcpStream;
2099
2100 let cfg = core::config::Config::load();
2101 if cfg.proxy_enabled != Some(true) {
2102 return;
2103 }
2104
2105 let port = crate::proxy_setup::default_port();
2106 let already_running = {
2107 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2108 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
2109 TcpStream::connect_timeout(&addr, crate::proxy_setup::proxy_timeout()).is_ok()
2110 };
2111
2112 if already_running {
2113 tracing::debug!("proxy already running on port {port}");
2114 return;
2115 }
2116
2117 let binary = core::portable_binary::resolve_portable_binary();
2118
2119 match std::process::Command::new(&binary)
2120 .args(["proxy", "start", &format!("--port={port}")])
2121 .stdin(std::process::Stdio::null())
2122 .stdout(std::process::Stdio::null())
2123 .stderr(std::process::Stdio::null())
2124 .spawn()
2125 {
2126 Ok(_) => tracing::info!("auto-started proxy on port {port}"),
2127 Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
2128 }
2129}
2130
2131fn resolve_worker_threads(parallelism: usize) -> usize {
2132 std::env::var("LEAN_CTX_WORKER_THREADS")
2133 .ok()
2134 .and_then(|v| v.parse::<usize>().ok())
2135 .unwrap_or_else(|| parallelism.clamp(1, 4))
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140 use super::*;
2141 use serial_test::serial;
2142
2143 #[test]
2144 #[serial]
2145 fn worker_threads_default_clamps_low() {
2146 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2147 assert_eq!(resolve_worker_threads(1), 1);
2148 }
2149
2150 #[test]
2151 #[serial]
2152 fn worker_threads_default_clamps_high() {
2153 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2154 assert_eq!(resolve_worker_threads(32), 4);
2155 }
2156
2157 #[test]
2158 #[serial]
2159 fn worker_threads_default_passthrough() {
2160 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2161 assert_eq!(resolve_worker_threads(3), 3);
2162 }
2163
2164 #[test]
2165 #[serial]
2166 fn worker_threads_env_override() {
2167 std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
2168 assert_eq!(resolve_worker_threads(2), 12);
2169 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2170 }
2171
2172 #[test]
2173 #[serial]
2174 fn worker_threads_env_invalid_falls_back() {
2175 std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
2176 assert_eq!(resolve_worker_threads(3), 3);
2177 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2178 }
2179}