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