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