Skip to main content

lean_ctx/cli/
dispatch.rs

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    // On Linux, if the binary was replaced while running, systemd may write
11    // the path with " (deleted)" suffix into ExecStart, causing "(deleted)"
12    // to appear as an argument. Strip it defensively.
13    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                        if let Err(e) = crate::daemon::stop_daemon() {
703                            eprintln!("Error: {e}");
704                            std::process::exit(1);
705                        }
706                        return;
707                    }
708
709                    if status_mode {
710                        println!("{}", crate::daemon::daemon_status());
711                        return;
712                    }
713
714                    if daemon_mode {
715                        if let Err(e) = crate::daemon::start_daemon(&rest) {
716                            eprintln!("Error: {e}");
717                            std::process::exit(1);
718                        }
719                        return;
720                    }
721
722                    if foreground_daemon {
723                        if let Err(e) = crate::daemon::init_foreground_daemon() {
724                            eprintln!("Error writing PID file: {e}");
725                            std::process::exit(1);
726                        }
727                        let addr = crate::daemon::daemon_addr();
728                        if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
729                        {
730                            tracing::error!("Daemon server error: {e}");
731                            crate::daemon::cleanup_daemon_files();
732                            std::process::exit(1);
733                        }
734                        crate::daemon::cleanup_daemon_files();
735                        return;
736                    }
737
738                    if cfg.auth_token.is_none() {
739                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
740                            if !v.trim().is_empty() {
741                                cfg.auth_token = Some(v);
742                            }
743                        }
744                    }
745
746                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
747                        tracing::error!("HTTP server error: {e}");
748                        std::process::exit(1);
749                    }
750                    return;
751                }
752                #[cfg(not(feature = "http-server"))]
753                {
754                    eprintln!("lean-ctx serve is not available in this build");
755                    std::process::exit(1);
756                }
757            }
758            "watch" => {
759                if rest.iter().any(|a| a == "--help" || a == "-h") {
760                    println!("Usage: lean-ctx watch");
761                    println!("  Live TUI dashboard (real-time event stream).");
762                    return;
763                }
764                if let Err(e) = tui::run() {
765                    tracing::error!("TUI error: {e}");
766                    std::process::exit(1);
767                }
768                return;
769            }
770            "proxy" => {
771                #[cfg(feature = "http-server")]
772                {
773                    let sub = rest.first().map_or("help", std::string::String::as_str);
774                    match sub {
775                        "start" => {
776                            let port: u16 = rest
777                                .iter()
778                                .find_map(|p| {
779                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
780                                })
781                                .and_then(|p| p.parse().ok())
782                                .unwrap_or_else(crate::proxy_setup::default_port);
783                            let autostart = rest.iter().any(|a| a == "--autostart");
784                            if autostart {
785                                crate::proxy_autostart::install(port, false);
786                                return;
787                            }
788                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
789                                tracing::error!("Proxy error: {e}");
790                                std::process::exit(1);
791                            }
792                        }
793                        "stop" => {
794                            let port: u16 = rest
795                                .iter()
796                                .find_map(|p| p.strip_prefix("--port="))
797                                .and_then(|p| p.parse().ok())
798                                .unwrap_or_else(crate::proxy_setup::default_port);
799                            let health_url = format!("http://127.0.0.1:{port}/health");
800                            match ureq::get(&health_url).call() {
801                                Ok(resp) => {
802                                    if let Ok(body) = resp.into_body().read_to_string() {
803                                        if let Some(pid_str) = body
804                                            .split("pid\":")
805                                            .nth(1)
806                                            .and_then(|s| s.split([',', '}']).next())
807                                        {
808                                            if let Ok(pid) = pid_str.trim().parse::<u32>() {
809                                                let _ =
810                                                    crate::ipc::process::terminate_gracefully(pid);
811                                                std::thread::sleep(
812                                                    std::time::Duration::from_millis(500),
813                                                );
814                                                if crate::ipc::process::is_alive(pid) {
815                                                    let _ = crate::ipc::process::force_kill(pid);
816                                                }
817                                                println!(
818                                                    "Proxy on port {port} stopped (PID {pid})."
819                                                );
820                                                return;
821                                            }
822                                        }
823                                    }
824                                    println!("Proxy on port {port} running but could not parse PID. Use `lean-ctx stop` to kill all.");
825                                }
826                                Err(_) => {
827                                    println!("No proxy running on port {port}.");
828                                }
829                            }
830                        }
831                        "status" => {
832                            let port: u16 = rest
833                                .iter()
834                                .find_map(|p| p.strip_prefix("--port="))
835                                .and_then(|p| p.parse().ok())
836                                .unwrap_or_else(crate::proxy_setup::default_port);
837                            let cfg = crate::core::config::Config::load();
838                            println!("lean-ctx proxy:");
839                            match cfg.proxy_enabled {
840                                Some(true) => println!("  Config:  enabled"),
841                                Some(false) => println!("  Config:  disabled"),
842                                None => println!("  Config:  undecided (not yet configured)"),
843                            }
844                            println!("  Port:    {port}");
845                            if let Ok(resp) =
846                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
847                            {
848                                let body = resp.into_body().read_to_string().unwrap_or_default();
849                                println!("  Process: running");
850                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
851                                    println!("  Requests:    {}", v["requests_total"]);
852                                    println!("  Compressed:  {}", v["requests_compressed"]);
853                                    println!("  Tokens saved: {}", v["tokens_saved"]);
854                                    println!(
855                                        "  Compression: {}%",
856                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
857                                    );
858                                }
859                            } else {
860                                println!("  Process: not running");
861                            }
862                            if cfg.proxy_enabled == Some(false) || cfg.proxy_enabled.is_none() {
863                                println!();
864                                println!("  Enable: lean-ctx proxy enable");
865
866                                let home = dirs::home_dir().unwrap_or_default();
867                                if crate::proxy_setup::has_stale_proxy_url(&home) {
868                                    println!();
869                                    println!("  \x1b[33m⚠ WARNING: Claude Code ANTHROPIC_BASE_URL points to the local proxy,\x1b[0m");
870                                    println!("  \x1b[33m  but proxy is not enabled. This causes 401 auth failures.\x1b[0m");
871                                    println!("  Fix:  lean-ctx proxy cleanup   (remove stale URL)");
872                                    println!("        lean-ctx proxy enable    (enable the proxy)");
873                                }
874                            }
875                        }
876                        "enable" => {
877                            let force = rest.iter().any(|a| a == "--force");
878                            let mut cfg = crate::core::config::Config::load();
879                            cfg.proxy_enabled = Some(true);
880                            let _ = cfg.save();
881
882                            let port = crate::proxy_setup::default_port();
883                            crate::proxy_autostart::install(port, false);
884                            std::thread::sleep(std::time::Duration::from_millis(500));
885
886                            let home = dirs::home_dir().unwrap_or_default();
887                            crate::proxy_setup::install_proxy_env_unchecked(
888                                &home, port, false, force,
889                            );
890                            println!("\x1b[32m✓\x1b[0m Proxy enabled on port {port}. LLM requests will be compressed.");
891                        }
892                        "disable" => {
893                            let mut cfg = crate::core::config::Config::load();
894                            cfg.proxy_enabled = Some(false);
895                            let _ = cfg.save();
896
897                            crate::proxy_autostart::uninstall(false);
898                            let home = dirs::home_dir().unwrap_or_default();
899                            crate::proxy_setup::uninstall_proxy_env(&home, false);
900
901                            println!(
902                                "\x1b[32m✓\x1b[0m Proxy disabled. Original endpoint restored."
903                            );
904                            println!("  Re-enable anytime: lean-ctx proxy enable");
905                        }
906                        "cleanup" => {
907                            let home = dirs::home_dir().unwrap_or_default();
908                            let removed = crate::proxy_setup::cleanup_stale_proxy_env(&home);
909                            if removed > 0 {
910                                println!(
911                                    "\x1b[32m✓\x1b[0m Cleaned up {removed} stale proxy URL(s)."
912                                );
913                                println!("  Restart your AI tool for changes to take effect.");
914                            } else {
915                                println!("  No stale proxy URLs found. Nothing to clean up.");
916                            }
917                        }
918                        _ => {
919                            println!("Usage: lean-ctx proxy <start|stop|status|enable|disable|cleanup> [--port=4444]");
920                        }
921                    }
922                    return;
923                }
924                #[cfg(not(feature = "http-server"))]
925                {
926                    eprintln!("lean-ctx proxy is not available in this build");
927                    std::process::exit(1);
928                }
929            }
930            "init" => {
931                super::cmd_init(&rest);
932                return;
933            }
934            "setup" => {
935                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
936                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
937                let fix = rest.iter().any(|a| a == "--fix");
938                let json = rest.iter().any(|a| a == "--json");
939                let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
940
941                if non_interactive || fix || json || yes {
942                    let opts = setup::SetupOptions {
943                        non_interactive,
944                        yes,
945                        fix,
946                        json,
947                        no_auto_approve,
948                        ..Default::default()
949                    };
950                    match setup::run_setup_with_options(opts) {
951                        Ok(report) => {
952                            if json {
953                                println!(
954                                    "{}",
955                                    serde_json::to_string_pretty(&report)
956                                        .unwrap_or_else(|_| "{}".to_string())
957                                );
958                            }
959                            if !report.success {
960                                std::process::exit(1);
961                            }
962                        }
963                        Err(e) => {
964                            eprintln!("{e}");
965                            std::process::exit(1);
966                        }
967                    }
968                } else {
969                    setup::run_setup();
970                }
971                return;
972            }
973            "install" => {
974                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
975                let json = rest.iter().any(|a| a == "--json");
976                if !repair {
977                    eprintln!("Usage: lean-ctx install --repair [--json]");
978                    std::process::exit(1);
979                }
980                let opts = setup::SetupOptions {
981                    non_interactive: true,
982                    yes: true,
983                    fix: true,
984                    json,
985                    ..Default::default()
986                };
987                match setup::run_setup_with_options(opts) {
988                    Ok(report) => {
989                        if json {
990                            println!(
991                                "{}",
992                                serde_json::to_string_pretty(&report)
993                                    .unwrap_or_else(|_| "{}".to_string())
994                            );
995                        }
996                        if !report.success {
997                            std::process::exit(1);
998                        }
999                    }
1000                    Err(e) => {
1001                        eprintln!("{e}");
1002                        std::process::exit(1);
1003                    }
1004                }
1005                return;
1006            }
1007            "bootstrap" => {
1008                let json = rest.iter().any(|a| a == "--json");
1009                let opts = setup::SetupOptions {
1010                    non_interactive: true,
1011                    yes: true,
1012                    fix: true,
1013                    json,
1014                    ..Default::default()
1015                };
1016                match setup::run_setup_with_options(opts) {
1017                    Ok(report) => {
1018                        if json {
1019                            println!(
1020                                "{}",
1021                                serde_json::to_string_pretty(&report)
1022                                    .unwrap_or_else(|_| "{}".to_string())
1023                            );
1024                        }
1025                        if !report.success {
1026                            std::process::exit(1);
1027                        }
1028                    }
1029                    Err(e) => {
1030                        eprintln!("{e}");
1031                        std::process::exit(1);
1032                    }
1033                }
1034                return;
1035            }
1036            "status" => {
1037                let code = status::run_cli(&rest);
1038                if code != 0 {
1039                    std::process::exit(code);
1040                }
1041                return;
1042            }
1043            "read" => {
1044                super::cmd_read(&rest);
1045                core::stats::flush();
1046                return;
1047            }
1048            "diff" => {
1049                super::cmd_diff(&rest);
1050                core::stats::flush();
1051                return;
1052            }
1053            "grep" => {
1054                super::cmd_grep(&rest);
1055                core::stats::flush();
1056                return;
1057            }
1058            "find" => {
1059                super::cmd_find(&rest);
1060                core::stats::flush();
1061                return;
1062            }
1063            "ls" => {
1064                super::cmd_ls(&rest);
1065                core::stats::flush();
1066                return;
1067            }
1068            "deps" => {
1069                super::cmd_deps(&rest);
1070                core::stats::flush();
1071                return;
1072            }
1073            "discover" => {
1074                super::cmd_discover(&rest);
1075                return;
1076            }
1077            "ghost" => {
1078                super::cmd_ghost(&rest);
1079                return;
1080            }
1081            "filter" => {
1082                super::cmd_filter(&rest);
1083                return;
1084            }
1085            "heatmap" => {
1086                heatmap::cmd_heatmap(&rest);
1087                return;
1088            }
1089            "graph" => {
1090                let sub = rest.first().map_or("build", std::string::String::as_str);
1091                match sub {
1092                    "build" => {
1093                        let root = rest.get(1).cloned().or_else(|| {
1094                            std::env::current_dir()
1095                                .ok()
1096                                .map(|p| p.to_string_lossy().to_string())
1097                        });
1098                        let root = root.unwrap_or_else(|| ".".to_string());
1099                        let index = core::graph_index::load_or_build(&root);
1100                        println!(
1101                            "Graph built: {} files, {} edges",
1102                            index.files.len(),
1103                            index.edges.len()
1104                        );
1105                    }
1106                    "export-html" => {
1107                        let mut root: Option<String> = None;
1108                        let mut out: Option<String> = None;
1109                        let mut max_nodes: usize = 2500;
1110
1111                        let args = &rest[1..];
1112                        let mut i = 0usize;
1113                        while i < args.len() {
1114                            let a = args[i].as_str();
1115                            if let Some(v) = a.strip_prefix("--root=") {
1116                                root = Some(v.to_string());
1117                            } else if a == "--root" {
1118                                root = args.get(i + 1).cloned();
1119                                i += 1;
1120                            } else if let Some(v) = a.strip_prefix("--out=") {
1121                                out = Some(v.to_string());
1122                            } else if a == "--out" {
1123                                out = args.get(i + 1).cloned();
1124                                i += 1;
1125                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1126                                max_nodes = v.parse::<usize>().unwrap_or(0);
1127                            } else if a == "--max-nodes" {
1128                                let v = args.get(i + 1).map_or("", String::as_str);
1129                                max_nodes = v.parse::<usize>().unwrap_or(0);
1130                                i += 1;
1131                            }
1132                            i += 1;
1133                        }
1134
1135                        let root = root
1136                            .or_else(|| {
1137                                std::env::current_dir()
1138                                    .ok()
1139                                    .map(|p| p.to_string_lossy().to_string())
1140                            })
1141                            .unwrap_or_else(|| ".".to_string());
1142                        let Some(out) = out else {
1143                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1144                            std::process::exit(1);
1145                        };
1146                        if max_nodes == 0 {
1147                            eprintln!("--max-nodes must be >= 1");
1148                            std::process::exit(1);
1149                        }
1150
1151                        core::graph_export::export_graph_html(
1152                            &root,
1153                            std::path::Path::new(&out),
1154                            max_nodes,
1155                        )
1156                        .unwrap_or_else(|e| {
1157                            eprintln!("graph export failed: {e}");
1158                            std::process::exit(1);
1159                        });
1160                        println!("{out}");
1161                    }
1162                    _ => {
1163                        eprintln!(
1164                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1165                        );
1166                        std::process::exit(1);
1167                    }
1168                }
1169                return;
1170            }
1171            "smells" => {
1172                let action = rest.first().map_or("summary", String::as_str);
1173                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1174                    if let Some(v) = a.strip_prefix("--rule=") {
1175                        return Some(v.to_string());
1176                    }
1177                    if a == "--rule" {
1178                        return rest.get(i + 1).cloned();
1179                    }
1180                    None
1181                });
1182                let path = rest.iter().enumerate().find_map(|(i, a)| {
1183                    if let Some(v) = a.strip_prefix("--path=") {
1184                        return Some(v.to_string());
1185                    }
1186                    if a == "--path" {
1187                        return rest.get(i + 1).cloned();
1188                    }
1189                    None
1190                });
1191                let root = rest
1192                    .iter()
1193                    .enumerate()
1194                    .find_map(|(i, a)| {
1195                        if let Some(v) = a.strip_prefix("--root=") {
1196                            return Some(v.to_string());
1197                        }
1198                        if a == "--root" {
1199                            return rest.get(i + 1).cloned();
1200                        }
1201                        None
1202                    })
1203                    .or_else(|| {
1204                        std::env::current_dir()
1205                            .ok()
1206                            .map(|p| p.to_string_lossy().to_string())
1207                    })
1208                    .unwrap_or_else(|| ".".to_string());
1209                let fmt = if rest.iter().any(|a| a == "--json") {
1210                    Some("json")
1211                } else {
1212                    None
1213                };
1214                println!(
1215                    "{}",
1216                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1217                );
1218                return;
1219            }
1220            "session" => {
1221                super::cmd_session_action(&rest);
1222                return;
1223            }
1224            "ledger" => {
1225                super::cmd_ledger(&rest);
1226                return;
1227            }
1228            "control" | "context-control" => {
1229                super::cmd_control(&rest);
1230                return;
1231            }
1232            "plan" | "context-plan" => {
1233                super::cmd_plan(&rest);
1234                return;
1235            }
1236            "compile" | "context-compile" => {
1237                super::cmd_compile(&rest);
1238                return;
1239            }
1240            "knowledge" => {
1241                super::cmd_knowledge(&rest);
1242                return;
1243            }
1244            "overview" => {
1245                super::cmd_overview(&rest);
1246                return;
1247            }
1248            "compress" => {
1249                super::cmd_compress(&rest);
1250                return;
1251            }
1252            "wrapped" => {
1253                super::cmd_wrapped(&rest);
1254                return;
1255            }
1256            "sessions" => {
1257                super::cmd_sessions(&rest);
1258                return;
1259            }
1260            "benchmark" => {
1261                super::cmd_benchmark(&rest);
1262                return;
1263            }
1264            "profile" => {
1265                super::cmd_profile(&rest);
1266                return;
1267            }
1268            "config" => {
1269                super::cmd_config(&rest);
1270                return;
1271            }
1272            "stats" => {
1273                super::cmd_stats(&rest);
1274                return;
1275            }
1276            "cache" => {
1277                super::cmd_cache(&rest);
1278                return;
1279            }
1280            "theme" => {
1281                super::cmd_theme(&rest);
1282                return;
1283            }
1284            "tee" => {
1285                super::cmd_tee(&rest);
1286                return;
1287            }
1288            "terse" | "compression" => {
1289                super::cmd_compression(&rest);
1290                return;
1291            }
1292            "slow-log" => {
1293                super::cmd_slow_log(&rest);
1294                return;
1295            }
1296            "update" | "--self-update" => {
1297                core::updater::run(&rest);
1298                return;
1299            }
1300            "restart" => {
1301                cmd_restart();
1302                return;
1303            }
1304            "stop" => {
1305                cmd_stop();
1306                return;
1307            }
1308            "dev-install" => {
1309                cmd_dev_install();
1310                return;
1311            }
1312            "doctor" => {
1313                let code = doctor::run_cli(&rest);
1314                if code != 0 {
1315                    std::process::exit(code);
1316                }
1317                return;
1318            }
1319            "harden" => {
1320                super::harden::run(&rest);
1321                return;
1322            }
1323            "export-rules" => {
1324                super::export_rules::run(&rest);
1325                return;
1326            }
1327            "gotchas" | "bugs" => {
1328                super::cloud::cmd_gotchas(&rest);
1329                return;
1330            }
1331            "learn" => {
1332                super::cmd_learn(&rest);
1333                return;
1334            }
1335            "buddy" | "pet" => {
1336                super::cloud::cmd_buddy(&rest);
1337                return;
1338            }
1339            "hook" => {
1340                hook_handlers::mark_hook_environment();
1341                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1342                let action = rest.first().map_or("help", std::string::String::as_str);
1343                match action {
1344                    "rewrite" => hook_handlers::handle_rewrite(),
1345                    "redirect" => hook_handlers::handle_redirect(),
1346                    "observe" => hook_handlers::handle_observe(),
1347                    "copilot" => hook_handlers::handle_copilot(),
1348                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1349                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1350                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1351                    _ => {
1352                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1353                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1354                        std::process::exit(1);
1355                    }
1356                }
1357                return;
1358            }
1359            "report-issue" | "report" => {
1360                report::run(&rest);
1361                return;
1362            }
1363            "uninstall" => {
1364                let dry_run = rest.iter().any(|a| a == "--dry-run");
1365                uninstall::run(dry_run);
1366                return;
1367            }
1368            "bypass" => {
1369                if rest.is_empty() {
1370                    eprintln!("Usage: lean-ctx bypass \"command\"");
1371                    eprintln!("Runs the command with zero compression (raw passthrough).");
1372                    std::process::exit(1);
1373                }
1374                let command = if rest.len() == 1 {
1375                    rest[0].clone()
1376                } else {
1377                    shell::join_command(&args[2..])
1378                };
1379                std::env::set_var("LEAN_CTX_RAW", "1");
1380                let code = shell::exec(&command);
1381                std::process::exit(code);
1382            }
1383            "safety-levels" | "safety" => {
1384                println!("{}", core::compression_safety::format_safety_table());
1385                return;
1386            }
1387            "cheat" | "cheatsheet" | "cheat-sheet" => {
1388                super::cmd_cheatsheet();
1389                return;
1390            }
1391            "login" => {
1392                super::cloud::cmd_login(&rest);
1393                return;
1394            }
1395            "register" => {
1396                super::cloud::cmd_register(&rest);
1397                return;
1398            }
1399            "forgot-password" => {
1400                super::cloud::cmd_forgot_password(&rest);
1401                return;
1402            }
1403            "sync" => {
1404                super::cloud::cmd_sync();
1405                return;
1406            }
1407            "contribute" => {
1408                super::cloud::cmd_contribute();
1409                return;
1410            }
1411            "cloud" => {
1412                super::cloud::cmd_cloud(&rest);
1413                return;
1414            }
1415            "upgrade" => {
1416                super::cloud::cmd_upgrade();
1417                return;
1418            }
1419            "--version" | "-V" => {
1420                println!("{}", core::integrity::origin_line());
1421                return;
1422            }
1423            "--help" | "-h" => {
1424                print_help();
1425                return;
1426            }
1427            "mcp" => {}
1428            _ => {
1429                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1430                print_help();
1431                std::process::exit(1);
1432            }
1433        }
1434    }
1435
1436    if let Err(e) = run_mcp_server() {
1437        tracing::error!("lean-ctx: {e}");
1438        std::process::exit(1);
1439    }
1440}
1441
1442fn passthrough(command: &str) -> ! {
1443    let (shell, flag) = shell::shell_and_flag();
1444    let status = std::process::Command::new(&shell)
1445        .arg(&flag)
1446        .arg(command)
1447        .env("LEAN_CTX_ACTIVE", "1")
1448        .status()
1449        .map_or(127, |s| s.code().unwrap_or(1));
1450    std::process::exit(status);
1451}
1452
1453fn run_async<F: std::future::Future>(future: F) -> F::Output {
1454    tokio::runtime::Runtime::new()
1455        .expect("failed to create async runtime")
1456        .block_on(future)
1457}
1458
1459fn run_mcp_server() -> Result<()> {
1460    use rmcp::ServiceExt;
1461
1462    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1463
1464    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1465
1466    // Concurrency hardening:
1467    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1468    // - Limits Tokio worker/blocking threads to avoid host degradation.
1469    // - LEAN_CTX_WORKER_THREADS overrides the default for environments
1470    //   with many concurrent subagents (e.g. parallel review pipelines).
1471    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1472        "mcp-startup",
1473        std::time::Duration::from_secs(3),
1474        std::time::Duration::from_secs(30),
1475    );
1476
1477    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1478    let worker_threads = resolve_worker_threads(parallelism);
1479    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1480
1481    let rt = tokio::runtime::Builder::new_multi_thread()
1482        .worker_threads(worker_threads)
1483        .max_blocking_threads(max_blocking_threads)
1484        .enable_all()
1485        .build()?;
1486
1487    let server = tools::create_server();
1488    drop(startup_lock);
1489
1490    // Auto-start proxy in background so the dashboard gets exact token data.
1491    spawn_proxy_if_needed();
1492
1493    rt.block_on(async {
1494        core::logging::init_mcp_logging();
1495        core::protocol::set_mcp_context(true);
1496
1497        tracing::info!(
1498            "lean-ctx v{} MCP server starting",
1499            env!("CARGO_PKG_VERSION")
1500        );
1501
1502        let transport =
1503            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1504        let server_handle = server.clone();
1505        let service = match server.serve(transport).await {
1506            Ok(s) => s,
1507            Err(e) => {
1508                let msg = e.to_string();
1509                if msg.contains("expect initialized")
1510                    || msg.contains("context canceled")
1511                    || msg.contains("broken pipe")
1512                {
1513                    tracing::debug!("Client disconnected before init: {msg}");
1514                    return Ok(());
1515                }
1516                return Err(e.into());
1517            }
1518        };
1519        match service.waiting().await {
1520            Ok(reason) => {
1521                tracing::info!("MCP server stopped: {reason:?}");
1522            }
1523            Err(e) => {
1524                let msg = e.to_string();
1525                if msg.contains("broken pipe")
1526                    || msg.contains("connection reset")
1527                    || msg.contains("context canceled")
1528                {
1529                    tracing::info!("MCP server: transport closed ({msg})");
1530                } else {
1531                    tracing::error!("MCP server error: {msg}");
1532                }
1533            }
1534        }
1535
1536        server_handle.shutdown().await;
1537
1538        core::stats::flush();
1539        core::heatmap::flush();
1540        core::mode_predictor::ModePredictor::flush();
1541        core::feedback::FeedbackStore::flush();
1542
1543        Ok(())
1544    })
1545}
1546
1547fn print_help() {
1548    println!(
1549        "lean-ctx {version} — Context Runtime for AI Agents
1550
155160+ compression patterns | 61 MCP tools | 10 read modes | Context Continuity Protocol
1552
1553USAGE:
1554    lean-ctx                       Start MCP server (stdio)
1555    lean-ctx serve                 Start MCP server (Streamable HTTP)
1556    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1557    lean-ctx serve --stop          Stop running daemon
1558    lean-ctx serve --status        Show daemon status
1559    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1560    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1561    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1562    lean-ctx exec \"command\"        Same as -c
1563    lean-ctx shell                 Interactive shell with compression
1564
1565COMMANDS:
1566    gain                           Visual dashboard (colors, bars, sparklines, USD)
1567    gain --live                    Live mode: auto-refreshes every 1s in-place
1568    gain --graph                   30-day savings chart
1569    gain --daily                   Bordered day-by-day table with USD
1570    gain --json                    Raw JSON export of all stats
1571         token-report [--json]          Token + memory report (project + session + CEP)
1572    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1573    index <status|build|build-full|watch>  Codebase index utilities
1574    cep                            CEP impact report (score trends, cache, modes)
1575    watch                          Live TUI dashboard (real-time event stream)
1576    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1577    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1578    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1579    proxy status                   Show proxy statistics
1580    cache [list|clear|stats]       Show/manage file read cache
1581    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1582    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1583    benchmark run [path] [--json]  Run real benchmark on project files
1584    benchmark report [path]        Generate shareable Markdown report
1585    cheatsheet                     Command cheat sheet & workflow quick reference
1586    setup                          One-command setup: shell + editor + verify
1587    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1588    bootstrap                      Non-interactive setup + fix (zero-config)
1589    status [--json]                Show setup + MCP + rules status
1590    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1591    init --agent <name>            Configure MCP for specific editor/agent
1592    read <file> [-m mode]          Read file with compression
1593    diff <file1> <file2>           Compressed file diff
1594    grep <pattern> [path]          Search with compressed output
1595    find <pattern> [path]          Find files with compressed output
1596    ls [path]                      Directory listing with compression
1597    deps [path]                    Show project dependencies
1598    discover                       Find uncompressed commands in shell history
1599    ghost [--json]                 Ghost Token report: find hidden token waste
1600    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1601    session                        Show adoption statistics
1602    session task <desc>            Set current task
1603    session finding <summary>      Record a finding
1604    session save                   Save current session
1605    session load [id]              Load session (latest if no ID)
1606    knowledge remember <value> --category <c> --key <k>   Store a fact
1607    knowledge recall [query] [--category <c>]             Retrieve facts
1608    knowledge search <query>       Cross-project knowledge search
1609    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1610    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1611    knowledge remove --category <c> --key <k>             Remove a fact
1612    knowledge status               Knowledge base summary
1613    overview [task]                Project overview (task-contextualized if given)
1614    compress [--signatures]        Context compression checkpoint
1615    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1616    profile [list|show|diff|create|set]  Manage context profiles
1617    theme [list|set|export|import] Customize terminal colors and themes
1618    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1619    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1620    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1621    update [--check]               Self-update lean-ctx binary from GitHub Releases
1622    stop                           Stop ALL lean-ctx processes (daemon, proxy, orphans)
1623    restart                        Restart daemon (applies config.toml changes)
1624    dev-install                    Build release + atomic install + restart (for development)
1625    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1626    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1627    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1628    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1629    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1630                                   Code smell detection (Property Graph, 8 rules)
1631    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1632    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1633    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1634    uninstall                      Remove shell hook, MCP configs, and data directory
1635
1636SHELL HOOK PATTERNS (95+):
1637    git       status, log, diff, add, commit, push, pull, fetch, clone,
1638              branch, checkout, switch, merge, stash, tag, reset, remote
1639    docker    build, ps, images, logs, compose, exec, network
1640    npm/pnpm  install, test, run, list, outdated, audit
1641    cargo     build, test, check, clippy
1642    gh        pr list/view/create, issue list/view, run list/view
1643    kubectl   get pods/services/deployments, logs, describe, apply
1644    python    pip install/list/outdated, ruff check/format, poetry, uv
1645    linters   eslint, biome, prettier, golangci-lint
1646    builds    tsc, next build, vite build
1647    ruby      rubocop, bundle install/update, rake test, rails test
1648    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1649    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1650    utils     curl, grep/rg, find, ls, wget, env
1651    data      JSON schema extraction, log deduplication
1652
1653READ MODES:
1654    auto                           Auto-select optimal mode (default)
1655    full                           Full content (cached re-reads = 13 tokens)
1656    map                            Dependency graph + API signatures
1657    signatures                     tree-sitter AST extraction (18 languages)
1658    task                           Task-relevant filtering (requires ctx_session task)
1659    reference                      One-line reference stub (cheap cache key)
1660    aggressive                     Syntax-stripped content
1661    entropy                        Shannon entropy filtered
1662    diff                           Changed lines only
1663    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1664
1665ENVIRONMENT:
1666    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1667    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1668    LEAN_CTX_RAW=1                 Same as --raw for current command
1669    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1670    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1671
1672OPTIONS:
1673    --version, -V                  Show version
1674    --help, -h                     Show this help
1675
1676EXAMPLES:
1677    lean-ctx -c \"git status\"       Compressed git output
1678    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1679    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1680    lean-ctx gain                  Visual terminal dashboard
1681    lean-ctx gain --live           Live auto-updating terminal dashboard
1682    lean-ctx gain --graph          30-day savings chart
1683    lean-ctx gain --daily          Day-by-day breakdown with USD
1684         lean-ctx token-report --json   Machine-readable token + memory report
1685    lean-ctx dashboard             Open web dashboard at localhost:3333
1686    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1687    lean-ctx gain --wrapped        Wrapped report card (recommended)
1688    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1689    lean-ctx sessions list         List all CCP sessions
1690    lean-ctx sessions show         Show latest session state
1691    lean-ctx discover              Find missed savings in shell history
1692    lean-ctx setup                 One-command setup (shell + editors + verify)
1693    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1694    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1695    lean-ctx bootstrap --json      Machine-readable bootstrap report
1696    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1697    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1698    lean-ctx-off                   Disable all shell aliases
1699    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1700    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1701    lean-ctx-mode off              Same as lean-ctx-off
1702    lean-ctx-status                Show whether compression is active
1703    lean-ctx init --agent pi       Install Pi Coding Agent extension
1704    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1705    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1706    lean-ctx doctor --fix --json   Repair + machine-readable report
1707    lean-ctx status --json         Machine-readable current status
1708    lean-ctx session task \"implement auth\"
1709    lean-ctx session finding \"auth.rs:42 — missing validation\"
1710    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1711    lean-ctx knowledge recall \"authentication\"
1712    lean-ctx knowledge search \"database migration\"
1713    lean-ctx overview \"refactor auth module\"
1714    lean-ctx compress --signatures
1715    lean-ctx read src/main.rs -m map
1716    lean-ctx grep \"pub fn\" src/
1717    lean-ctx deps .
1718
1719CLOUD:
1720    cloud status                   Show cloud connection status
1721    login <email>                  Log into existing LeanCTX Cloud account
1722    register <email>               Create a new LeanCTX Cloud account
1723    forgot-password <email>        Send password reset email
1724    sync                           Upload local stats to cloud dashboard
1725    contribute                     Share anonymized compression data
1726
1727TROUBLESHOOTING:
1728    Commands broken?     lean-ctx-off             (fixes current session)
1729    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1730    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1731    Binary missing?      Aliases auto-fallback to original commands (safe)
1732    Preview init?        lean-ctx init --global --dry-run
1733
1734WEBSITE: https://leanctx.com
1735GITHUB:  https://github.com/yvgude/lean-ctx
1736",
1737        version = env!("CARGO_PKG_VERSION"),
1738    );
1739}
1740
1741fn cmd_stop() {
1742    use crate::daemon;
1743    use crate::ipc;
1744
1745    eprintln!("Stopping all lean-ctx processes…");
1746
1747    // 1. Unload LaunchAgent/systemd first to prevent respawning
1748    crate::proxy_autostart::stop();
1749    eprintln!("  Unloaded autostart (LaunchAgent/systemd).");
1750
1751    // 2. Stop daemon via IPC
1752    if let Err(e) = daemon::stop_daemon() {
1753        eprintln!("  Warning: daemon stop: {e}");
1754    }
1755
1756    // 3. SIGTERM all remaining lean-ctx processes
1757    let killed = ipc::process::kill_all_by_name("lean-ctx");
1758    if killed > 0 {
1759        eprintln!("  Sent SIGTERM to {killed} process(es).");
1760    }
1761
1762    std::thread::sleep(std::time::Duration::from_millis(500));
1763
1764    // 4. Force-kill stragglers (but never MCP servers — IDE will respawn them)
1765    let remaining = ipc::process::find_killable_pids("lean-ctx");
1766    if !remaining.is_empty() {
1767        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1768        for &pid in &remaining {
1769            let _ = ipc::process::force_kill(pid);
1770        }
1771        std::thread::sleep(std::time::Duration::from_millis(300));
1772    }
1773
1774    daemon::cleanup_daemon_files();
1775
1776    let final_check = ipc::process::find_killable_pids("lean-ctx");
1777    if final_check.is_empty() {
1778        eprintln!("  ✓ All lean-ctx processes stopped.");
1779    } else {
1780        eprintln!(
1781            "  ✗ {} process(es) could not be killed: {:?}",
1782            final_check.len(),
1783            final_check
1784        );
1785        eprintln!(
1786            "    Try: sudo kill -9 {}",
1787            final_check
1788                .iter()
1789                .map(std::string::ToString::to_string)
1790                .collect::<Vec<_>>()
1791                .join(" ")
1792        );
1793        std::process::exit(1);
1794    }
1795}
1796
1797fn cmd_restart() {
1798    use crate::daemon;
1799    use crate::ipc;
1800
1801    eprintln!("Restarting lean-ctx…");
1802
1803    // Stop autostart first to prevent respawning during restart
1804    crate::proxy_autostart::stop();
1805
1806    if let Err(e) = daemon::stop_daemon() {
1807        eprintln!("  Warning: daemon stop: {e}");
1808    }
1809
1810    let orphans = ipc::process::kill_all_by_name("lean-ctx");
1811    if orphans > 0 {
1812        eprintln!("  Terminated {orphans} orphan process(es).");
1813    }
1814
1815    std::thread::sleep(std::time::Duration::from_millis(500));
1816
1817    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1818    if !remaining.is_empty() {
1819        eprintln!(
1820            "  Force-killing {} stubborn process(es): {:?}",
1821            remaining.len(),
1822            remaining
1823        );
1824        for &pid in &remaining {
1825            let _ = ipc::process::force_kill(pid);
1826        }
1827        std::thread::sleep(std::time::Duration::from_millis(300));
1828    }
1829
1830    daemon::cleanup_daemon_files();
1831
1832    // Re-enable autostart
1833    crate::proxy_autostart::start();
1834
1835    match daemon::start_daemon(&[]) {
1836        Ok(()) => eprintln!("  ✓ Daemon restarted. Config changes are now active."),
1837        Err(e) => {
1838            eprintln!("  ✗ Daemon start failed: {e}");
1839            std::process::exit(1);
1840        }
1841    }
1842}
1843
1844fn cmd_dev_install() {
1845    use crate::ipc;
1846
1847    let cargo_root = find_cargo_project_root();
1848    let Some(cargo_root) = cargo_root else {
1849        eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1850        std::process::exit(1);
1851    };
1852
1853    eprintln!("Building release binary…");
1854    let build = std::process::Command::new("cargo")
1855        .args(["build", "--release"])
1856        .current_dir(&cargo_root)
1857        .status();
1858
1859    match build {
1860        Ok(s) if s.success() => {}
1861        Ok(s) => {
1862            eprintln!("  Build failed with exit code {}", s.code().unwrap_or(-1));
1863            std::process::exit(1);
1864        }
1865        Err(e) => {
1866            eprintln!("  Build failed: {e}");
1867            std::process::exit(1);
1868        }
1869    }
1870
1871    let built_binary = cargo_root.join("target/release/lean-ctx");
1872    if !built_binary.exists() {
1873        eprintln!(
1874            "  Error: Built binary not found at {}",
1875            built_binary.display()
1876        );
1877        std::process::exit(1);
1878    }
1879
1880    let install_path = resolve_install_path();
1881    eprintln!("Installing to {}…", install_path.display());
1882
1883    eprintln!("  Stopping all lean-ctx processes…");
1884    crate::proxy_autostart::stop();
1885    let _ = crate::daemon::stop_daemon();
1886    ipc::process::kill_all_by_name("lean-ctx");
1887    std::thread::sleep(std::time::Duration::from_millis(500));
1888
1889    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1890    if !remaining.is_empty() {
1891        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1892        for &pid in &remaining {
1893            let _ = ipc::process::force_kill(pid);
1894        }
1895        std::thread::sleep(std::time::Duration::from_millis(500));
1896    }
1897
1898    let old_path = install_path.with_extension("old");
1899    if install_path.exists() {
1900        if let Err(e) = std::fs::rename(&install_path, &old_path) {
1901            eprintln!("  Warning: rename existing binary: {e}");
1902        }
1903    }
1904
1905    match std::fs::copy(&built_binary, &install_path) {
1906        Ok(_) => {
1907            let _ = std::fs::remove_file(&old_path);
1908            #[cfg(unix)]
1909            {
1910                use std::os::unix::fs::PermissionsExt;
1911                let _ =
1912                    std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1913            }
1914            eprintln!("  ✓ Binary installed.");
1915        }
1916        Err(e) => {
1917            eprintln!("  Error: copy failed: {e}");
1918            if old_path.exists() {
1919                let _ = std::fs::rename(&old_path, &install_path);
1920                eprintln!("  Rolled back to previous binary.");
1921            }
1922            std::process::exit(1);
1923        }
1924    }
1925
1926    let version = std::process::Command::new(&install_path)
1927        .arg("--version")
1928        .output()
1929        .map_or_else(
1930            |_| "unknown".to_string(),
1931            |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1932        );
1933
1934    eprintln!("  ✓ dev-install complete: {version}");
1935
1936    eprintln!("  Re-enabling autostart…");
1937    crate::proxy_autostart::start();
1938
1939    eprintln!("  Starting daemon…");
1940    match crate::daemon::start_daemon(&[]) {
1941        Ok(()) => {}
1942        Err(e) => eprintln!("  Warning: daemon start: {e} (will be started by editor)"),
1943    }
1944}
1945
1946fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1947    let mut dir = std::env::current_dir().ok()?;
1948    loop {
1949        if dir.join("Cargo.toml").exists() {
1950            return Some(dir);
1951        }
1952        if !dir.pop() {
1953            return None;
1954        }
1955    }
1956}
1957
1958fn resolve_install_path() -> std::path::PathBuf {
1959    if let Ok(exe) = std::env::current_exe() {
1960        if let Ok(canonical) = exe.canonicalize() {
1961            let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1962            if !is_in_cargo_target && canonical.exists() {
1963                return canonical;
1964            }
1965        }
1966    }
1967
1968    if let Ok(home) = std::env::var("HOME") {
1969        let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1970        if local_bin.parent().is_some_and(std::path::Path::exists) {
1971            return local_bin;
1972        }
1973    }
1974
1975    std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1976}
1977
1978fn spawn_proxy_if_needed() {
1979    use std::net::TcpStream;
1980    use std::time::Duration;
1981
1982    let cfg = core::config::Config::load();
1983    if cfg.proxy_enabled != Some(true) {
1984        return;
1985    }
1986
1987    let port = crate::proxy_setup::default_port();
1988    let already_running = TcpStream::connect_timeout(
1989        &format!("127.0.0.1:{port}").parse().unwrap(),
1990        Duration::from_millis(200),
1991    )
1992    .is_ok();
1993
1994    if already_running {
1995        tracing::debug!("proxy already running on port {port}");
1996        return;
1997    }
1998
1999    let binary = core::portable_binary::resolve_portable_binary();
2000
2001    match std::process::Command::new(&binary)
2002        .args(["proxy", "start", &format!("--port={port}")])
2003        .stdin(std::process::Stdio::null())
2004        .stdout(std::process::Stdio::null())
2005        .stderr(std::process::Stdio::null())
2006        .spawn()
2007    {
2008        Ok(_) => tracing::info!("auto-started proxy on port {port}"),
2009        Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
2010    }
2011}
2012
2013fn resolve_worker_threads(parallelism: usize) -> usize {
2014    std::env::var("LEAN_CTX_WORKER_THREADS")
2015        .ok()
2016        .and_then(|v| v.parse::<usize>().ok())
2017        .unwrap_or_else(|| parallelism.clamp(1, 4))
2018}
2019
2020#[cfg(test)]
2021mod tests {
2022    use super::*;
2023    use serial_test::serial;
2024
2025    #[test]
2026    #[serial]
2027    fn worker_threads_default_clamps_low() {
2028        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2029        assert_eq!(resolve_worker_threads(1), 1);
2030    }
2031
2032    #[test]
2033    #[serial]
2034    fn worker_threads_default_clamps_high() {
2035        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2036        assert_eq!(resolve_worker_threads(32), 4);
2037    }
2038
2039    #[test]
2040    #[serial]
2041    fn worker_threads_default_passthrough() {
2042        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2043        assert_eq!(resolve_worker_threads(3), 3);
2044    }
2045
2046    #[test]
2047    #[serial]
2048    fn worker_threads_env_override() {
2049        std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
2050        assert_eq!(resolve_worker_threads(2), 12);
2051        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2052    }
2053
2054    #[test]
2055    #[serial]
2056    fn worker_threads_env_invalid_falls_back() {
2057        std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
2058        assert_eq!(resolve_worker_threads(3), 3);
2059        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2060    }
2061}