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                    "related" | "impact" | "symbol" | "context" | "status" => {
1225                        let path_arg = if sub == "status" {
1226                            None
1227                        } else {
1228                            rest.get(1).map(String::as_str)
1229                        };
1230                        let root_idx = if sub == "status" { 1 } else { 2 };
1231                        let root = resolve_graph_root(rest.get(root_idx));
1232                        println!(
1233                            "{}",
1234                            tools::ctx_graph::handle(
1235                                sub,
1236                                path_arg,
1237                                &root,
1238                                &mut core::cache::SessionCache::new(),
1239                                tools::CrpMode::Off,
1240                                None,
1241                                None,
1242                            )
1243                        );
1244                    }
1245                    _ => {
1246                        eprintln!(
1247                            "Usage:\n  \
1248                             lean-ctx graph build [path]\n  \
1249                             lean-ctx graph related <file>\n  \
1250                             lean-ctx graph impact <file|symbol>\n  \
1251                             lean-ctx graph symbol <name>\n  \
1252                             lean-ctx graph context <query>\n  \
1253                             lean-ctx graph status\n  \
1254                             lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1255                        );
1256                        std::process::exit(1);
1257                    }
1258                }
1259                return;
1260            }
1261            "smells" => {
1262                let action = rest.first().map_or("summary", String::as_str);
1263                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1264                    if let Some(v) = a.strip_prefix("--rule=") {
1265                        return Some(v.to_string());
1266                    }
1267                    if a == "--rule" {
1268                        return rest.get(i + 1).cloned();
1269                    }
1270                    None
1271                });
1272                let path = rest.iter().enumerate().find_map(|(i, a)| {
1273                    if let Some(v) = a.strip_prefix("--path=") {
1274                        return Some(v.to_string());
1275                    }
1276                    if a == "--path" {
1277                        return rest.get(i + 1).cloned();
1278                    }
1279                    None
1280                });
1281                let root = rest
1282                    .iter()
1283                    .enumerate()
1284                    .find_map(|(i, a)| {
1285                        if let Some(v) = a.strip_prefix("--root=") {
1286                            return Some(v.to_string());
1287                        }
1288                        if a == "--root" {
1289                            return rest.get(i + 1).cloned();
1290                        }
1291                        None
1292                    })
1293                    .or_else(|| {
1294                        std::env::current_dir()
1295                            .ok()
1296                            .map(|p| p.to_string_lossy().to_string())
1297                    })
1298                    .unwrap_or_else(|| ".".to_string());
1299                let fmt = if rest.iter().any(|a| a == "--json") {
1300                    Some("json")
1301                } else {
1302                    None
1303                };
1304                println!(
1305                    "{}",
1306                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1307                );
1308                return;
1309            }
1310            "session" => {
1311                super::cmd_session_action(&rest);
1312                return;
1313            }
1314            "ledger" => {
1315                super::cmd_ledger(&rest);
1316                return;
1317            }
1318            "control" | "context-control" => {
1319                super::cmd_control(&rest);
1320                return;
1321            }
1322            "plan" | "context-plan" => {
1323                super::cmd_plan(&rest);
1324                return;
1325            }
1326            "compile" | "context-compile" => {
1327                super::cmd_compile(&rest);
1328                return;
1329            }
1330            "knowledge" => {
1331                super::cmd_knowledge(&rest);
1332                return;
1333            }
1334            "overview" => {
1335                super::cmd_overview(&rest);
1336                return;
1337            }
1338            "compress" => {
1339                super::cmd_compress(&rest);
1340                return;
1341            }
1342            "wrapped" => {
1343                super::cmd_wrapped(&rest);
1344                return;
1345            }
1346            "sessions" => {
1347                super::cmd_sessions(&rest);
1348                return;
1349            }
1350            "benchmark" => {
1351                super::cmd_benchmark(&rest);
1352                return;
1353            }
1354            "profile" => {
1355                super::cmd_profile(&rest);
1356                return;
1357            }
1358            "config" => {
1359                super::cmd_config(&rest);
1360                return;
1361            }
1362            "stats" => {
1363                super::cmd_stats(&rest);
1364                return;
1365            }
1366            "cache" => {
1367                super::cmd_cache(&rest);
1368                return;
1369            }
1370            "theme" => {
1371                super::cmd_theme(&rest);
1372                return;
1373            }
1374            "tee" => {
1375                super::cmd_tee(&rest);
1376                return;
1377            }
1378            "terse" | "compression" => {
1379                super::cmd_compression(&rest);
1380                return;
1381            }
1382            "slow-log" => {
1383                super::cmd_slow_log(&rest);
1384                return;
1385            }
1386            "update" | "--self-update" => {
1387                core::updater::run(&rest);
1388                return;
1389            }
1390            "restart" => {
1391                cmd_restart();
1392                return;
1393            }
1394            "stop" => {
1395                cmd_stop();
1396                return;
1397            }
1398            "dev-install" => {
1399                cmd_dev_install();
1400                return;
1401            }
1402            "doctor" => {
1403                let code = doctor::run_cli(&rest);
1404                if code != 0 {
1405                    std::process::exit(code);
1406                }
1407                return;
1408            }
1409            "harden" => {
1410                super::harden::run(&rest);
1411                return;
1412            }
1413            "export-rules" => {
1414                super::export_rules::run(&rest);
1415                return;
1416            }
1417            "gotchas" | "bugs" => {
1418                super::cloud::cmd_gotchas(&rest);
1419                return;
1420            }
1421            "learn" => {
1422                super::cmd_learn(&rest);
1423                return;
1424            }
1425            "buddy" | "pet" => {
1426                super::cloud::cmd_buddy(&rest);
1427                return;
1428            }
1429            "hook" => {
1430                hook_handlers::mark_hook_environment();
1431                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1432                let action = rest.first().map_or("help", std::string::String::as_str);
1433                match action {
1434                    "rewrite" => hook_handlers::handle_rewrite(),
1435                    "redirect" => hook_handlers::handle_redirect(),
1436                    "observe" => hook_handlers::handle_observe(),
1437                    "copilot" => hook_handlers::handle_copilot(),
1438                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1439                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1440                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1441                    _ => {
1442                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1443                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1444                        std::process::exit(1);
1445                    }
1446                }
1447                return;
1448            }
1449            "report-issue" | "report" => {
1450                report::run(&rest);
1451                return;
1452            }
1453            "uninstall" => {
1454                let dry_run = rest.iter().any(|a| a == "--dry-run");
1455                let keep_config = rest.iter().any(|a| a == "--keep-config");
1456                uninstall::run(dry_run, keep_config);
1457                return;
1458            }
1459            "bypass" => {
1460                if rest.is_empty() {
1461                    eprintln!("Usage: lean-ctx bypass \"command\"");
1462                    eprintln!("Runs the command with zero compression (raw passthrough).");
1463                    std::process::exit(1);
1464                }
1465                let command = if rest.len() == 1 {
1466                    rest[0].clone()
1467                } else {
1468                    shell::join_command(&args[2..])
1469                };
1470                std::env::set_var("LEAN_CTX_RAW", "1");
1471                let code = shell::exec(&command);
1472                std::process::exit(code);
1473            }
1474            "safety-levels" | "safety" => {
1475                println!("{}", core::compression_safety::format_safety_table());
1476                return;
1477            }
1478            "cheat" | "cheatsheet" | "cheat-sheet" => {
1479                super::cmd_cheatsheet();
1480                return;
1481            }
1482            "login" => {
1483                super::cloud::cmd_login(&rest);
1484                return;
1485            }
1486            "register" => {
1487                super::cloud::cmd_register(&rest);
1488                return;
1489            }
1490            "forgot-password" => {
1491                super::cloud::cmd_forgot_password(&rest);
1492                return;
1493            }
1494            "sync" => {
1495                super::cloud::cmd_sync();
1496                return;
1497            }
1498            "contribute" => {
1499                super::cloud::cmd_contribute();
1500                return;
1501            }
1502            "cloud" => {
1503                super::cloud::cmd_cloud(&rest);
1504                return;
1505            }
1506            "upgrade" => {
1507                super::cloud::cmd_upgrade();
1508                return;
1509            }
1510            "--version" | "-V" => {
1511                println!("{}", core::integrity::origin_line());
1512                return;
1513            }
1514            "--help" | "-h" => {
1515                print_help();
1516                return;
1517            }
1518            "mcp" => {}
1519            _ => {
1520                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1521                print_help();
1522                std::process::exit(1);
1523            }
1524        }
1525    }
1526
1527    if let Err(e) = run_mcp_server() {
1528        tracing::error!("lean-ctx: {e}");
1529        std::process::exit(1);
1530    }
1531}
1532
1533fn resolve_graph_root(arg: Option<&String>) -> String {
1534    arg.cloned()
1535        .or_else(|| {
1536            std::env::current_dir()
1537                .ok()
1538                .map(|p| p.to_string_lossy().to_string())
1539        })
1540        .unwrap_or_else(|| ".".to_string())
1541}
1542
1543fn passthrough(command: &str) -> ! {
1544    let (shell, flag) = shell::shell_and_flag();
1545    let mut cmd = std::process::Command::new(&shell);
1546    cmd.arg(&flag).arg(command).env("LEAN_CTX_ACTIVE", "1");
1547    shell::platform::apply_utf8_locale(&mut cmd);
1548    let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
1549    std::process::exit(status);
1550}
1551
1552fn run_async<F: std::future::Future>(future: F) -> F::Output {
1553    tokio::runtime::Runtime::new()
1554        .expect("failed to create async runtime")
1555        .block_on(future)
1556}
1557
1558fn run_mcp_server() -> Result<()> {
1559    use rmcp::ServiceExt;
1560
1561    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1562
1563    crate::core::startup_guard::crash_loop_backoff(crate::core::startup_guard::MCP_PROCESS_NAME);
1564
1565    // Concurrency hardening:
1566    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1567    // - Limits Tokio worker/blocking threads to avoid host degradation.
1568    // - LEAN_CTX_WORKER_THREADS overrides the default for environments
1569    //   with many concurrent subagents (e.g. parallel review pipelines).
1570    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1571        "mcp-startup",
1572        std::time::Duration::from_secs(3),
1573        std::time::Duration::from_secs(30),
1574    );
1575
1576    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1577    let worker_threads = resolve_worker_threads(parallelism);
1578    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1579
1580    let rt = tokio::runtime::Builder::new_multi_thread()
1581        .worker_threads(worker_threads)
1582        .max_blocking_threads(max_blocking_threads)
1583        .enable_all()
1584        .build()?;
1585
1586    let server = tools::create_server();
1587    drop(startup_lock);
1588
1589    // Auto-start proxy in background so the dashboard gets exact token data.
1590    spawn_proxy_if_needed();
1591
1592    rt.block_on(async {
1593        core::logging::init_mcp_logging();
1594        core::protocol::set_mcp_context(true);
1595
1596        tracing::info!(
1597            "lean-ctx v{} MCP server starting",
1598            env!("CARGO_PKG_VERSION")
1599        );
1600
1601        let transport =
1602            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1603        let server_handle = server.clone();
1604        let service = match server.serve(transport).await {
1605            Ok(s) => s,
1606            Err(e) => {
1607                let msg = e.to_string();
1608                if msg.contains("expect initialized")
1609                    || msg.contains("context canceled")
1610                    || msg.contains("broken pipe")
1611                {
1612                    tracing::debug!("Client disconnected before init: {msg}");
1613                    return Ok(());
1614                }
1615                return Err(e.into());
1616            }
1617        };
1618        match service.waiting().await {
1619            Ok(reason) => {
1620                tracing::info!("MCP server stopped: {reason:?}");
1621            }
1622            Err(e) => {
1623                let msg = e.to_string();
1624                if msg.contains("broken pipe")
1625                    || msg.contains("connection reset")
1626                    || msg.contains("context canceled")
1627                {
1628                    tracing::info!("MCP server: transport closed ({msg})");
1629                } else {
1630                    tracing::error!("MCP server error: {msg}");
1631                }
1632            }
1633        }
1634
1635        server_handle.shutdown().await;
1636
1637        core::stats::flush();
1638        core::heatmap::flush();
1639        core::mode_predictor::ModePredictor::flush();
1640        core::feedback::FeedbackStore::flush();
1641
1642        Ok(())
1643    })
1644}
1645
1646fn print_help() {
1647    println!(
1648        "lean-ctx {version} — Context Runtime for AI Agents
1649
165060+ compression patterns | 61 MCP tools | 10 read modes | Context Continuity Protocol
1651
1652USAGE:
1653    lean-ctx                       Start MCP server (stdio)
1654    lean-ctx serve                 Start MCP server (Streamable HTTP)
1655    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1656    lean-ctx serve --stop          Stop running daemon
1657    lean-ctx serve --status        Show daemon status
1658    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1659    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1660    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1661    lean-ctx exec \"command\"        Same as -c
1662    lean-ctx shell                 Interactive shell with compression
1663
1664COMMANDS:
1665    gain                           Visual dashboard (colors, bars, sparklines, USD)
1666    gain --live                    Live mode: auto-refreshes every 1s in-place
1667    gain --graph                   30-day savings chart
1668    gain --daily                   Bordered day-by-day table with USD
1669    gain --json                    Raw JSON export of all stats
1670         token-report [--json]          Token + memory report (project + session + CEP)
1671    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1672    index <status|build|build-full|watch>  Codebase index utilities
1673    cep                            CEP impact report (score trends, cache, modes)
1674    watch                          Live TUI dashboard (real-time event stream)
1675    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1676    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1677    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1678    proxy status                   Show proxy statistics
1679    daemon start|stop|status       IPC daemon management
1680    daemon enable|disable          Auto-start daemon on login (systemd/LaunchAgent)
1681    cache [list|clear|stats]       Show/manage file read cache
1682    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1683    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1684    benchmark run [path] [--json]  Run real benchmark on project files
1685    benchmark report [path]        Generate shareable Markdown report
1686    cheatsheet                     Command cheat sheet & workflow quick reference
1687    setup                          One-command setup: shell + editor + verify
1688    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1689    bootstrap                      Non-interactive setup + fix (zero-config)
1690    status [--json]                Show setup + MCP + rules status
1691    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1692    init --agent <name>            Configure MCP for specific editor/agent
1693    read <file> [-m mode]          Read file with compression
1694    diff <file1> <file2>           Compressed file diff
1695    grep <pattern> [path]          Search with compressed output
1696    find <pattern> [path]          Find files with compressed output
1697    ls [path]                      Directory listing with compression
1698    deps [path]                    Show project dependencies
1699    discover                       Find uncompressed commands in shell history
1700    ghost [--json]                 Ghost Token report: find hidden token waste
1701    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1702    session                        Show adoption statistics
1703    session task <desc>            Set current task
1704    session finding <summary>      Record a finding
1705    session save                   Save current session
1706    session load [id]              Load session (latest if no ID)
1707    knowledge remember <value> --category <c> --key <k>   Store a fact
1708    knowledge recall [query] [--category <c>]             Retrieve facts
1709    knowledge search <query>       Cross-project knowledge search
1710    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1711    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1712    knowledge remove --category <c> --key <k>             Remove a fact
1713    knowledge status               Knowledge base summary
1714    overview [task]                Project overview (task-contextualized if given)
1715    compress [--signatures]        Context compression checkpoint
1716    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1717    profile [list|show|diff|create|set]  Manage context profiles
1718    theme [list|set|export|import] Customize terminal colors and themes
1719    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1720    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1721    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1722    update [--check]               Self-update lean-ctx binary from GitHub Releases
1723    stop                           Stop ALL lean-ctx processes (daemon, proxy, orphans)
1724    restart                        Restart daemon (applies config.toml changes)
1725    dev-install                    Build release + atomic install + restart (for development)
1726    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1727    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1728    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1729    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1730    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1731                                   Code smell detection (Property Graph, 8 rules)
1732    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1733    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1734    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1735    uninstall [--keep-config]       Remove all lean-ctx artifacts (--keep-config preserves MCP/rules)
1736
1737SHELL HOOK PATTERNS (95+):
1738    git       status, log, diff, add, commit, push, pull, fetch, clone,
1739              branch, checkout, switch, merge, stash, tag, reset, remote
1740    docker    build, ps, images, logs, compose, exec, network
1741    npm/pnpm  install, test, run, list, outdated, audit
1742    cargo     build, test, check, clippy
1743    gh        pr list/view/create, issue list/view, run list/view
1744    kubectl   get pods/services/deployments, logs, describe, apply
1745    python    pip install/list/outdated, ruff check/format, poetry, uv
1746    linters   eslint, biome, prettier, golangci-lint
1747    builds    tsc, next build, vite build
1748    ruby      rubocop, bundle install/update, rake test, rails test
1749    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1750    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1751    utils     curl, grep/rg, find, ls, wget, env
1752    data      JSON schema extraction, log deduplication
1753
1754READ MODES:
1755    auto                           Auto-select optimal mode (default)
1756    full                           Full content (cached re-reads = 13 tokens)
1757    map                            Dependency graph + API signatures
1758    signatures                     tree-sitter AST extraction (18 languages)
1759    task                           Task-relevant filtering (requires ctx_session task)
1760    reference                      One-line reference stub (cheap cache key)
1761    aggressive                     Syntax-stripped content
1762    entropy                        Shannon entropy filtered
1763    diff                           Changed lines only
1764    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1765
1766ENVIRONMENT:
1767    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1768    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1769    LEAN_CTX_RAW=1                 Same as --raw for current command
1770    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1771    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1772
1773OPTIONS:
1774    --version, -V                  Show version
1775    --help, -h                     Show this help
1776
1777EXAMPLES:
1778    lean-ctx -c \"git status\"       Compressed git output
1779    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1780    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1781    lean-ctx gain                  Visual terminal dashboard
1782    lean-ctx gain --live           Live auto-updating terminal dashboard
1783    lean-ctx gain --graph          30-day savings chart
1784    lean-ctx gain --daily          Day-by-day breakdown with USD
1785         lean-ctx token-report --json   Machine-readable token + memory report
1786    lean-ctx dashboard             Open web dashboard at localhost:3333
1787    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1788    lean-ctx gain --wrapped        Wrapped report card (recommended)
1789    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1790    lean-ctx sessions list         List all CCP sessions
1791    lean-ctx sessions show         Show latest session state
1792    lean-ctx discover              Find missed savings in shell history
1793    lean-ctx setup                 One-command setup (shell + editors + verify)
1794    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1795    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1796    lean-ctx bootstrap --json      Machine-readable bootstrap report
1797    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1798    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1799    lean-ctx-off                   Disable all shell aliases
1800    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1801    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1802    lean-ctx-mode off              Same as lean-ctx-off
1803    lean-ctx-status                Show whether compression is active
1804    lean-ctx init --agent pi       Install Pi Coding Agent extension
1805    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1806    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1807    lean-ctx doctor --fix --json   Repair + machine-readable report
1808    lean-ctx status --json         Machine-readable current status
1809    lean-ctx session task \"implement auth\"
1810    lean-ctx session finding \"auth.rs:42 — missing validation\"
1811    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1812    lean-ctx knowledge recall \"authentication\"
1813    lean-ctx knowledge search \"database migration\"
1814    lean-ctx overview \"refactor auth module\"
1815    lean-ctx compress --signatures
1816    lean-ctx read src/main.rs -m map
1817    lean-ctx grep \"pub fn\" src/
1818    lean-ctx deps .
1819
1820GRAPH (project analysis):
1821    lean-ctx graph build [path]    Build/rebuild project graph index
1822    lean-ctx graph status          Show graph index statistics
1823    lean-ctx graph related <file>  List files related to a given file
1824    lean-ctx graph impact <file>   Show files impacted by changes to a file
1825    lean-ctx graph symbol <spec>   Inspect a symbol (format: file.rs::fn_name)
1826    lean-ctx graph context <query> Query the property graph for a concept
1827
1828CLOUD:
1829    cloud status                   Show cloud connection status
1830    login <email>                  Log into existing LeanCTX Cloud account
1831    register <email>               Create a new LeanCTX Cloud account
1832    forgot-password <email>        Send password reset email
1833    sync                           Upload local stats to cloud dashboard
1834    contribute                     Share anonymized compression data
1835
1836TROUBLESHOOTING:
1837    Commands broken?     lean-ctx-off             (fixes current session)
1838    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1839    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1840    Binary missing?      Aliases auto-fallback to original commands (safe)
1841    Preview init?        lean-ctx init --global --dry-run
1842
1843WEBSITE: https://leanctx.com
1844GITHUB:  https://github.com/yvgude/lean-ctx
1845",
1846        version = env!("CARGO_PKG_VERSION"),
1847    );
1848}
1849
1850fn cmd_stop() {
1851    use crate::daemon;
1852    use crate::ipc;
1853
1854    eprintln!("Stopping all lean-ctx processes…");
1855
1856    crate::proxy_autostart::stop();
1857    crate::daemon_autostart::stop();
1858    eprintln!("  Unloaded autostart (LaunchAgent/systemd).");
1859
1860    // 2. Stop daemon via IPC
1861    if let Err(e) = daemon::stop_daemon() {
1862        eprintln!("  Warning: daemon stop: {e}");
1863    }
1864
1865    // 3. SIGTERM all remaining lean-ctx processes
1866    let killed = ipc::process::kill_all_by_name("lean-ctx");
1867    if killed > 0 {
1868        eprintln!("  Sent SIGTERM to {killed} process(es).");
1869    }
1870
1871    std::thread::sleep(std::time::Duration::from_millis(500));
1872
1873    // 4. Force-kill stragglers (but never MCP servers — IDE will respawn them)
1874    let remaining = ipc::process::find_killable_pids("lean-ctx");
1875    if !remaining.is_empty() {
1876        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1877        for &pid in &remaining {
1878            let _ = ipc::process::force_kill(pid);
1879        }
1880        std::thread::sleep(std::time::Duration::from_millis(300));
1881    }
1882
1883    daemon::cleanup_daemon_files();
1884
1885    let final_check = ipc::process::find_killable_pids("lean-ctx");
1886    if final_check.is_empty() {
1887        eprintln!("  ✓ All lean-ctx processes stopped.");
1888    } else {
1889        eprintln!(
1890            "  ✗ {} process(es) could not be killed: {:?}",
1891            final_check.len(),
1892            final_check
1893        );
1894        eprintln!(
1895            "    Try: sudo kill -9 {}",
1896            final_check
1897                .iter()
1898                .map(std::string::ToString::to_string)
1899                .collect::<Vec<_>>()
1900                .join(" ")
1901        );
1902        std::process::exit(1);
1903    }
1904}
1905
1906fn cmd_restart() {
1907    use crate::daemon;
1908    use crate::ipc;
1909
1910    eprintln!("Restarting lean-ctx…");
1911
1912    crate::proxy_autostart::stop();
1913    crate::daemon_autostart::stop();
1914
1915    if let Err(e) = daemon::stop_daemon() {
1916        eprintln!("  Warning: daemon stop: {e}");
1917    }
1918
1919    let orphans = ipc::process::kill_all_by_name("lean-ctx");
1920    if orphans > 0 {
1921        eprintln!("  Terminated {orphans} orphan process(es).");
1922    }
1923
1924    std::thread::sleep(std::time::Duration::from_millis(500));
1925
1926    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1927    if !remaining.is_empty() {
1928        eprintln!(
1929            "  Force-killing {} stubborn process(es): {:?}",
1930            remaining.len(),
1931            remaining
1932        );
1933        for &pid in &remaining {
1934            let _ = ipc::process::force_kill(pid);
1935        }
1936        std::thread::sleep(std::time::Duration::from_millis(300));
1937    }
1938
1939    daemon::cleanup_daemon_files();
1940
1941    crate::proxy_autostart::start();
1942
1943    if crate::daemon_autostart::is_installed() {
1944        crate::daemon_autostart::start();
1945        eprintln!("  ✓ Daemon restarted via autostart.");
1946    } else {
1947        match daemon::start_daemon(&[]) {
1948            Ok(()) => eprintln!("  ✓ Daemon restarted."),
1949            Err(e) => {
1950                eprintln!("  ✗ Daemon start failed: {e}");
1951                std::process::exit(1);
1952            }
1953        }
1954    }
1955}
1956
1957fn cmd_dev_install() {
1958    use crate::ipc;
1959
1960    let cargo_root = find_cargo_project_root();
1961    let Some(cargo_root) = cargo_root else {
1962        eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1963        std::process::exit(1);
1964    };
1965
1966    eprintln!("Building release binary…");
1967    let build = std::process::Command::new("cargo")
1968        .args(["build", "--release"])
1969        .current_dir(&cargo_root)
1970        .status();
1971
1972    match build {
1973        Ok(s) if s.success() => {}
1974        Ok(s) => {
1975            eprintln!("  Build failed with exit code {}", s.code().unwrap_or(-1));
1976            std::process::exit(1);
1977        }
1978        Err(e) => {
1979            eprintln!("  Build failed: {e}");
1980            std::process::exit(1);
1981        }
1982    }
1983
1984    let built_binary = cargo_root.join("target/release/lean-ctx");
1985    if !built_binary.exists() {
1986        eprintln!(
1987            "  Error: Built binary not found at {}",
1988            built_binary.display()
1989        );
1990        std::process::exit(1);
1991    }
1992
1993    let install_path = resolve_install_path();
1994    eprintln!("Installing to {}…", install_path.display());
1995
1996    eprintln!("  Stopping all lean-ctx processes…");
1997    crate::proxy_autostart::stop();
1998    crate::daemon_autostart::stop();
1999    let _ = crate::daemon::stop_daemon();
2000    ipc::process::kill_all_by_name("lean-ctx");
2001    std::thread::sleep(std::time::Duration::from_millis(500));
2002
2003    let remaining = ipc::process::find_pids_by_name("lean-ctx");
2004    if !remaining.is_empty() {
2005        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
2006        for &pid in &remaining {
2007            let _ = ipc::process::force_kill(pid);
2008        }
2009        std::thread::sleep(std::time::Duration::from_millis(500));
2010    }
2011
2012    let old_path = install_path.with_extension("old");
2013    if install_path.exists() {
2014        if let Err(e) = std::fs::rename(&install_path, &old_path) {
2015            eprintln!("  Warning: rename existing binary: {e}");
2016        }
2017    }
2018
2019    match std::fs::copy(&built_binary, &install_path) {
2020        Ok(_) => {
2021            let _ = std::fs::remove_file(&old_path);
2022            #[cfg(unix)]
2023            {
2024                use std::os::unix::fs::PermissionsExt;
2025                let _ =
2026                    std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
2027            }
2028            eprintln!("  ✓ Binary installed.");
2029        }
2030        Err(e) => {
2031            eprintln!("  Error: copy failed: {e}");
2032            if old_path.exists() {
2033                let _ = std::fs::rename(&old_path, &install_path);
2034                eprintln!("  Rolled back to previous binary.");
2035            }
2036            std::process::exit(1);
2037        }
2038    }
2039
2040    let version = std::process::Command::new(&install_path)
2041        .arg("--version")
2042        .output()
2043        .map_or_else(
2044            |_| "unknown".to_string(),
2045            |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
2046        );
2047
2048    eprintln!("  ✓ dev-install complete: {version}");
2049
2050    eprintln!("  Re-enabling autostart…");
2051    crate::proxy_autostart::start();
2052
2053    if crate::daemon_autostart::is_installed() {
2054        crate::daemon_autostart::start();
2055        eprintln!("  ✓ Daemon restarted via autostart.");
2056    } else {
2057        eprintln!("  Starting daemon…");
2058        match crate::daemon::start_daemon(&[]) {
2059            Ok(()) => {}
2060            Err(e) => eprintln!("  Warning: daemon start: {e} (will be started by editor)"),
2061        }
2062    }
2063}
2064
2065fn find_cargo_project_root() -> Option<std::path::PathBuf> {
2066    let mut dir = std::env::current_dir().ok()?;
2067    loop {
2068        if dir.join("Cargo.toml").exists() {
2069            return Some(dir);
2070        }
2071        if !dir.pop() {
2072            return None;
2073        }
2074    }
2075}
2076
2077fn resolve_install_path() -> std::path::PathBuf {
2078    if let Ok(exe) = std::env::current_exe() {
2079        if let Ok(canonical) = exe.canonicalize() {
2080            let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
2081            if !is_in_cargo_target && canonical.exists() {
2082                return canonical;
2083            }
2084        }
2085    }
2086
2087    if let Ok(home) = std::env::var("HOME") {
2088        let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
2089        if local_bin.parent().is_some_and(std::path::Path::exists) {
2090            return local_bin;
2091        }
2092    }
2093
2094    std::path::PathBuf::from("/usr/local/bin/lean-ctx")
2095}
2096
2097fn spawn_proxy_if_needed() {
2098    use std::net::TcpStream;
2099
2100    let cfg = core::config::Config::load();
2101    if cfg.proxy_enabled != Some(true) {
2102        return;
2103    }
2104
2105    let port = crate::proxy_setup::default_port();
2106    let already_running = {
2107        use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2108        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
2109        TcpStream::connect_timeout(&addr, crate::proxy_setup::proxy_timeout()).is_ok()
2110    };
2111
2112    if already_running {
2113        tracing::debug!("proxy already running on port {port}");
2114        return;
2115    }
2116
2117    let binary = core::portable_binary::resolve_portable_binary();
2118
2119    match std::process::Command::new(&binary)
2120        .args(["proxy", "start", &format!("--port={port}")])
2121        .stdin(std::process::Stdio::null())
2122        .stdout(std::process::Stdio::null())
2123        .stderr(std::process::Stdio::null())
2124        .spawn()
2125    {
2126        Ok(_) => tracing::info!("auto-started proxy on port {port}"),
2127        Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
2128    }
2129}
2130
2131fn resolve_worker_threads(parallelism: usize) -> usize {
2132    std::env::var("LEAN_CTX_WORKER_THREADS")
2133        .ok()
2134        .and_then(|v| v.parse::<usize>().ok())
2135        .unwrap_or_else(|| parallelism.clamp(1, 4))
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140    use super::*;
2141    use serial_test::serial;
2142
2143    #[test]
2144    #[serial]
2145    fn worker_threads_default_clamps_low() {
2146        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2147        assert_eq!(resolve_worker_threads(1), 1);
2148    }
2149
2150    #[test]
2151    #[serial]
2152    fn worker_threads_default_clamps_high() {
2153        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2154        assert_eq!(resolve_worker_threads(32), 4);
2155    }
2156
2157    #[test]
2158    #[serial]
2159    fn worker_threads_default_passthrough() {
2160        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2161        assert_eq!(resolve_worker_threads(3), 3);
2162    }
2163
2164    #[test]
2165    #[serial]
2166    fn worker_threads_env_override() {
2167        std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
2168        assert_eq!(resolve_worker_threads(2), 12);
2169        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2170    }
2171
2172    #[test]
2173    #[serial]
2174    fn worker_threads_env_invalid_falls_back() {
2175        std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
2176        assert_eq!(resolve_worker_threads(3), 3);
2177        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2178    }
2179}