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
10    if args.len() > 1 {
11        let rest = args[2..].to_vec();
12
13        match args[1].as_str() {
14            "-c" | "exec" => {
15                let raw = rest.first().map(|a| a == "--raw").unwrap_or(false);
16                let cmd_args = if raw { &args[3..] } else { &args[2..] };
17                let command = if cmd_args.len() == 1 {
18                    cmd_args[0].clone()
19                } else {
20                    shell::join_command(cmd_args)
21                };
22                if std::env::var("LEAN_CTX_ACTIVE").is_ok()
23                    || std::env::var("LEAN_CTX_DISABLED").is_ok()
24                {
25                    passthrough(&command);
26                }
27                if raw {
28                    std::env::set_var("LEAN_CTX_RAW", "1");
29                } else {
30                    std::env::set_var("LEAN_CTX_COMPRESS", "1");
31                }
32                let code = shell::exec(&command);
33                core::stats::flush();
34                std::process::exit(code);
35            }
36            "-t" | "--track" => {
37                let cmd_args = &args[2..];
38                let command = if cmd_args.len() == 1 {
39                    cmd_args[0].clone()
40                } else {
41                    shell::join_command(cmd_args)
42                };
43                if std::env::var("LEAN_CTX_ACTIVE").is_ok()
44                    || std::env::var("LEAN_CTX_DISABLED").is_ok()
45                {
46                    passthrough(&command);
47                }
48                let code = shell::exec(&command);
49                core::stats::flush();
50                std::process::exit(code);
51            }
52            "shell" | "--shell" => {
53                shell::interactive();
54                return;
55            }
56            "gain" => {
57                if rest.iter().any(|a| a == "--reset") {
58                    core::stats::reset_all();
59                    println!("Stats reset. All token savings data cleared.");
60                    return;
61                }
62                if rest.iter().any(|a| a == "--live" || a == "--watch") {
63                    core::stats::gain_live();
64                    return;
65                }
66                let model = rest.iter().enumerate().find_map(|(i, a)| {
67                    if let Some(v) = a.strip_prefix("--model=") {
68                        return Some(v.to_string());
69                    }
70                    if a == "--model" {
71                        return rest.get(i + 1).cloned();
72                    }
73                    None
74                });
75                let period = rest
76                    .iter()
77                    .enumerate()
78                    .find_map(|(i, a)| {
79                        if let Some(v) = a.strip_prefix("--period=") {
80                            return Some(v.to_string());
81                        }
82                        if a == "--period" {
83                            return rest.get(i + 1).cloned();
84                        }
85                        None
86                    })
87                    .unwrap_or_else(|| "all".to_string());
88                let limit = rest
89                    .iter()
90                    .enumerate()
91                    .find_map(|(i, a)| {
92                        if let Some(v) = a.strip_prefix("--limit=") {
93                            return v.parse::<usize>().ok();
94                        }
95                        if a == "--limit" {
96                            return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
97                        }
98                        None
99                    })
100                    .unwrap_or(10);
101
102                if rest.iter().any(|a| a == "--graph") {
103                    println!("{}", core::stats::format_gain_graph());
104                } else if rest.iter().any(|a| a == "--daily") {
105                    println!("{}", core::stats::format_gain_daily());
106                } else if rest.iter().any(|a| a == "--json") {
107                    println!(
108                        "{}",
109                        tools::ctx_gain::handle(
110                            "json",
111                            Some(&period),
112                            model.as_deref(),
113                            Some(limit)
114                        )
115                    );
116                } else if rest.iter().any(|a| a == "--score") {
117                    println!(
118                        "{}",
119                        tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
120                    );
121                } else if rest.iter().any(|a| a == "--cost") {
122                    println!(
123                        "{}",
124                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
125                    );
126                } else if rest.iter().any(|a| a == "--tasks") {
127                    println!(
128                        "{}",
129                        tools::ctx_gain::handle("tasks", None, None, Some(limit))
130                    );
131                } else if rest.iter().any(|a| a == "--agents") {
132                    println!(
133                        "{}",
134                        tools::ctx_gain::handle("agents", None, None, Some(limit))
135                    );
136                } else if rest.iter().any(|a| a == "--heatmap") {
137                    println!(
138                        "{}",
139                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
140                    );
141                } else if rest.iter().any(|a| a == "--wrapped") {
142                    println!(
143                        "{}",
144                        tools::ctx_gain::handle(
145                            "wrapped",
146                            Some(&period),
147                            model.as_deref(),
148                            Some(limit)
149                        )
150                    );
151                } else if rest.iter().any(|a| a == "--pipeline") {
152                    let stats_path = dirs::home_dir()
153                        .unwrap_or_default()
154                        .join(".lean-ctx")
155                        .join("pipeline_stats.json");
156                    if let Ok(data) = std::fs::read_to_string(&stats_path) {
157                        if let Ok(stats) =
158                            serde_json::from_str::<core::pipeline::PipelineStats>(&data)
159                        {
160                            println!("{}", stats.format_summary());
161                        } else {
162                            println!("No pipeline stats available yet (corrupt data).");
163                        }
164                    } else {
165                        println!(
166                            "No pipeline stats available yet. Use MCP tools to generate data."
167                        );
168                    }
169                } else if rest.iter().any(|a| a == "--deep") {
170                    println!(
171                        "{}\n{}\n{}\n{}\n{}",
172                        tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
173                        tools::ctx_gain::handle("tasks", None, None, Some(limit)),
174                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
175                        tools::ctx_gain::handle("agents", None, None, Some(limit)),
176                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
177                    );
178                } else {
179                    println!("{}", core::stats::format_gain());
180                }
181                return;
182            }
183            "token-report" | "report-tokens" => {
184                let code = token_report::run_cli(&rest);
185                if code != 0 {
186                    std::process::exit(code);
187                }
188                return;
189            }
190            "cep" => {
191                println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
192                return;
193            }
194            "dashboard" => {
195                let port = rest
196                    .iter()
197                    .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
198                    .and_then(|p| p.parse().ok());
199                let host = rest
200                    .iter()
201                    .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
202                    .map(String::from);
203                let project = rest
204                    .iter()
205                    .find_map(|p| p.strip_prefix("--project="))
206                    .map(String::from);
207                if let Some(ref p) = project {
208                    std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
209                }
210                run_async(dashboard::start(port, host));
211                return;
212            }
213            "serve" => {
214                #[cfg(feature = "http-server")]
215                {
216                    let mut cfg = crate::http_server::HttpServerConfig::default();
217                    let mut i = 0;
218                    while i < rest.len() {
219                        match rest[i].as_str() {
220                            "--host" | "-H" => {
221                                i += 1;
222                                if i < rest.len() {
223                                    cfg.host = rest[i].clone();
224                                }
225                            }
226                            arg if arg.starts_with("--host=") => {
227                                cfg.host = arg["--host=".len()..].to_string();
228                            }
229                            "--port" | "-p" => {
230                                i += 1;
231                                if i < rest.len() {
232                                    if let Ok(p) = rest[i].parse::<u16>() {
233                                        cfg.port = p;
234                                    }
235                                }
236                            }
237                            arg if arg.starts_with("--port=") => {
238                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
239                                    cfg.port = p;
240                                }
241                            }
242                            "--project-root" => {
243                                i += 1;
244                                if i < rest.len() {
245                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
246                                }
247                            }
248                            arg if arg.starts_with("--project-root=") => {
249                                cfg.project_root =
250                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
251                            }
252                            "--auth-token" => {
253                                i += 1;
254                                if i < rest.len() {
255                                    cfg.auth_token = Some(rest[i].clone());
256                                }
257                            }
258                            arg if arg.starts_with("--auth-token=") => {
259                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
260                            }
261                            "--stateful" => cfg.stateful_mode = true,
262                            "--stateless" => cfg.stateful_mode = false,
263                            "--json" => cfg.json_response = true,
264                            "--sse" => cfg.json_response = false,
265                            "--disable-host-check" => cfg.disable_host_check = true,
266                            "--allowed-host" => {
267                                i += 1;
268                                if i < rest.len() {
269                                    cfg.allowed_hosts.push(rest[i].clone());
270                                }
271                            }
272                            arg if arg.starts_with("--allowed-host=") => {
273                                cfg.allowed_hosts
274                                    .push(arg["--allowed-host=".len()..].to_string());
275                            }
276                            "--max-body-bytes" => {
277                                i += 1;
278                                if i < rest.len() {
279                                    if let Ok(n) = rest[i].parse::<usize>() {
280                                        cfg.max_body_bytes = n;
281                                    }
282                                }
283                            }
284                            arg if arg.starts_with("--max-body-bytes=") => {
285                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
286                                    cfg.max_body_bytes = n;
287                                }
288                            }
289                            "--max-concurrency" => {
290                                i += 1;
291                                if i < rest.len() {
292                                    if let Ok(n) = rest[i].parse::<usize>() {
293                                        cfg.max_concurrency = n;
294                                    }
295                                }
296                            }
297                            arg if arg.starts_with("--max-concurrency=") => {
298                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
299                                    cfg.max_concurrency = n;
300                                }
301                            }
302                            "--max-rps" => {
303                                i += 1;
304                                if i < rest.len() {
305                                    if let Ok(n) = rest[i].parse::<u32>() {
306                                        cfg.max_rps = n;
307                                    }
308                                }
309                            }
310                            arg if arg.starts_with("--max-rps=") => {
311                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
312                                    cfg.max_rps = n;
313                                }
314                            }
315                            "--rate-burst" => {
316                                i += 1;
317                                if i < rest.len() {
318                                    if let Ok(n) = rest[i].parse::<u32>() {
319                                        cfg.rate_burst = n;
320                                    }
321                                }
322                            }
323                            arg if arg.starts_with("--rate-burst=") => {
324                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
325                                    cfg.rate_burst = n;
326                                }
327                            }
328                            "--request-timeout-ms" => {
329                                i += 1;
330                                if i < rest.len() {
331                                    if let Ok(n) = rest[i].parse::<u64>() {
332                                        cfg.request_timeout_ms = n;
333                                    }
334                                }
335                            }
336                            arg if arg.starts_with("--request-timeout-ms=") => {
337                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
338                                    cfg.request_timeout_ms = n;
339                                }
340                            }
341                            "--help" | "-h" => {
342                                eprintln!(
343                                    "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR]\\n\\
344                                     \\n\\
345                                     Options:\\n\\
346                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
347                                       --port, -p            Bind port (default: 8080)\\n\\
348                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
349                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
350                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
351                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
352                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
353                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
354                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
355                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
356                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
357                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
358                                       --disable-host-check  Disable Host header validation (unsafe)"
359                                );
360                                return;
361                            }
362                            _ => {}
363                        }
364                        i += 1;
365                    }
366
367                    if cfg.auth_token.is_none() {
368                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
369                            if !v.trim().is_empty() {
370                                cfg.auth_token = Some(v);
371                            }
372                        }
373                    }
374
375                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
376                        eprintln!("HTTP server error: {e}");
377                        std::process::exit(1);
378                    }
379                    return;
380                }
381                #[cfg(not(feature = "http-server"))]
382                {
383                    eprintln!("lean-ctx serve is not available in this build");
384                    std::process::exit(1);
385                }
386            }
387            "watch" => {
388                if let Err(e) = tui::run() {
389                    eprintln!("TUI error: {e}");
390                    std::process::exit(1);
391                }
392                return;
393            }
394            "proxy" => {
395                #[cfg(feature = "http-server")]
396                {
397                    let sub = rest.first().map(|s| s.as_str()).unwrap_or("help");
398                    match sub {
399                        "start" => {
400                            let port: u16 = rest
401                                .iter()
402                                .find_map(|p| {
403                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
404                                })
405                                .and_then(|p| p.parse().ok())
406                                .unwrap_or(4444);
407                            let autostart = rest.iter().any(|a| a == "--autostart");
408                            if autostart {
409                                crate::proxy_autostart::install(port, false);
410                                return;
411                            }
412                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
413                                eprintln!("Proxy error: {e}");
414                                std::process::exit(1);
415                            }
416                        }
417                        "stop" => {
418                            match ureq::get(&format!(
419                                "http://127.0.0.1:{}/health",
420                                rest.iter()
421                                    .find_map(|p| p.strip_prefix("--port="))
422                                    .and_then(|p| p.parse::<u16>().ok())
423                                    .unwrap_or(4444)
424                            ))
425                            .call()
426                            {
427                                Ok(_) => {
428                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
429                                }
430                                Err(_) => {
431                                    println!("No proxy running on that port.");
432                                }
433                            }
434                        }
435                        "status" => {
436                            let port: u16 = rest
437                                .iter()
438                                .find_map(|p| p.strip_prefix("--port="))
439                                .and_then(|p| p.parse().ok())
440                                .unwrap_or(4444);
441                            match ureq::get(&format!("http://127.0.0.1:{port}/status")).call() {
442                                Ok(resp) => {
443                                    let body =
444                                        resp.into_body().read_to_string().unwrap_or_default();
445                                    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body)
446                                    {
447                                        println!("lean-ctx proxy status:");
448                                        println!("  Requests:    {}", v["requests_total"]);
449                                        println!("  Compressed:  {}", v["requests_compressed"]);
450                                        println!("  Tokens saved: {}", v["tokens_saved"]);
451                                        println!(
452                                            "  Compression: {}%",
453                                            v["compression_ratio_pct"].as_str().unwrap_or("0.0")
454                                        );
455                                    } else {
456                                        println!("{body}");
457                                    }
458                                }
459                                Err(_) => {
460                                    println!("No proxy running on port {port}.");
461                                    println!("Start with: lean-ctx proxy start");
462                                }
463                            }
464                        }
465                        _ => {
466                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
467                        }
468                    }
469                    return;
470                }
471                #[cfg(not(feature = "http-server"))]
472                {
473                    eprintln!("lean-ctx proxy is not available in this build");
474                    std::process::exit(1);
475                }
476            }
477            "init" => {
478                super::cmd_init(&rest);
479                return;
480            }
481            "setup" => {
482                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
483                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
484                let fix = rest.iter().any(|a| a == "--fix");
485                let json = rest.iter().any(|a| a == "--json");
486
487                if non_interactive || fix || json || yes {
488                    let opts = setup::SetupOptions {
489                        non_interactive,
490                        yes,
491                        fix,
492                        json,
493                    };
494                    match setup::run_setup_with_options(opts) {
495                        Ok(report) => {
496                            if json {
497                                println!(
498                                    "{}",
499                                    serde_json::to_string_pretty(&report)
500                                        .unwrap_or_else(|_| "{}".to_string())
501                                );
502                            }
503                            if !report.success {
504                                std::process::exit(1);
505                            }
506                        }
507                        Err(e) => {
508                            eprintln!("{e}");
509                            std::process::exit(1);
510                        }
511                    }
512                } else {
513                    setup::run_setup();
514                }
515                return;
516            }
517            "bootstrap" => {
518                let json = rest.iter().any(|a| a == "--json");
519                let opts = setup::SetupOptions {
520                    non_interactive: true,
521                    yes: true,
522                    fix: true,
523                    json,
524                };
525                match setup::run_setup_with_options(opts) {
526                    Ok(report) => {
527                        if json {
528                            println!(
529                                "{}",
530                                serde_json::to_string_pretty(&report)
531                                    .unwrap_or_else(|_| "{}".to_string())
532                            );
533                        }
534                        if !report.success {
535                            std::process::exit(1);
536                        }
537                    }
538                    Err(e) => {
539                        eprintln!("{e}");
540                        std::process::exit(1);
541                    }
542                }
543                return;
544            }
545            "status" => {
546                let code = status::run_cli(&rest);
547                if code != 0 {
548                    std::process::exit(code);
549                }
550                return;
551            }
552            "read" => {
553                super::cmd_read(&rest);
554                return;
555            }
556            "diff" => {
557                super::cmd_diff(&rest);
558                return;
559            }
560            "grep" => {
561                super::cmd_grep(&rest);
562                return;
563            }
564            "find" => {
565                super::cmd_find(&rest);
566                return;
567            }
568            "ls" => {
569                super::cmd_ls(&rest);
570                return;
571            }
572            "deps" => {
573                super::cmd_deps(&rest);
574                return;
575            }
576            "discover" => {
577                super::cmd_discover(&rest);
578                return;
579            }
580            "ghost" => {
581                super::cmd_ghost(&rest);
582                return;
583            }
584            "filter" => {
585                super::cmd_filter(&rest);
586                return;
587            }
588            "heatmap" => {
589                heatmap::cmd_heatmap(&rest);
590                return;
591            }
592            "graph" => {
593                let mut action = "build";
594                let mut path_arg: Option<&str> = None;
595                for arg in &rest {
596                    if arg == "build" {
597                        action = "build";
598                    } else {
599                        path_arg = Some(arg.as_str());
600                    }
601                }
602                let root = path_arg
603                    .map(String::from)
604                    .or_else(|| {
605                        std::env::current_dir()
606                            .ok()
607                            .map(|p| p.to_string_lossy().to_string())
608                    })
609                    .unwrap_or_else(|| ".".to_string());
610                match action {
611                    "build" => {
612                        let index = core::graph_index::load_or_build(&root);
613                        println!(
614                            "Graph built: {} files, {} edges",
615                            index.files.len(),
616                            index.edges.len()
617                        );
618                    }
619                    _ => {
620                        eprintln!("Usage: lean-ctx graph [build] [path]");
621                    }
622                }
623                return;
624            }
625            "session" => {
626                super::cmd_session();
627                return;
628            }
629            "wrapped" => {
630                super::cmd_wrapped(&rest);
631                return;
632            }
633            "sessions" => {
634                super::cmd_sessions(&rest);
635                return;
636            }
637            "benchmark" => {
638                super::cmd_benchmark(&rest);
639                return;
640            }
641            "config" => {
642                super::cmd_config(&rest);
643                return;
644            }
645            "stats" => {
646                super::cmd_stats(&rest);
647                return;
648            }
649            "cache" => {
650                super::cmd_cache(&rest);
651                return;
652            }
653            "theme" => {
654                super::cmd_theme(&rest);
655                return;
656            }
657            "tee" => {
658                super::cmd_tee(&rest);
659                return;
660            }
661            "terse" => {
662                super::cmd_terse(&rest);
663                return;
664            }
665            "slow-log" => {
666                super::cmd_slow_log(&rest);
667                return;
668            }
669            "update" | "--self-update" => {
670                core::updater::run(&rest);
671                return;
672            }
673            "doctor" => {
674                let code = doctor::run_cli(&rest);
675                if code != 0 {
676                    std::process::exit(code);
677                }
678                return;
679            }
680            "gotchas" | "bugs" => {
681                super::cloud::cmd_gotchas(&rest);
682                return;
683            }
684            "buddy" | "pet" => {
685                super::cloud::cmd_buddy(&rest);
686                return;
687            }
688            "hook" => {
689                let action = rest.first().map(|s| s.as_str()).unwrap_or("help");
690                match action {
691                    "rewrite" => hook_handlers::handle_rewrite(),
692                    "redirect" => hook_handlers::handle_redirect(),
693                    "copilot" => hook_handlers::handle_copilot(),
694                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
695                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
696                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
697                    _ => {
698                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
699                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
700                        std::process::exit(1);
701                    }
702                }
703                return;
704            }
705            "report-issue" | "report" => {
706                report::run(&rest);
707                return;
708            }
709            "uninstall" => {
710                uninstall::run();
711                return;
712            }
713            "cheat" | "cheatsheet" | "cheat-sheet" => {
714                super::cmd_cheatsheet();
715                return;
716            }
717            "login" => {
718                super::cloud::cmd_login(&rest);
719                return;
720            }
721            "register" => {
722                super::cloud::cmd_register(&rest);
723                return;
724            }
725            "forgot-password" => {
726                super::cloud::cmd_forgot_password(&rest);
727                return;
728            }
729            "sync" => {
730                super::cloud::cmd_sync();
731                return;
732            }
733            "contribute" => {
734                super::cloud::cmd_contribute();
735                return;
736            }
737            "cloud" => {
738                super::cloud::cmd_cloud(&rest);
739                return;
740            }
741            "upgrade" => {
742                super::cloud::cmd_upgrade();
743                return;
744            }
745            "--version" | "-V" => {
746                println!("{}", core::integrity::origin_line());
747                return;
748            }
749            "--help" | "-h" => {
750                print_help();
751                return;
752            }
753            "mcp" => {}
754            _ => {
755                eprintln!("lean-ctx: unknown command '{}'\n", args[1]);
756                print_help();
757                std::process::exit(1);
758            }
759        }
760    }
761
762    if let Err(e) = run_mcp_server() {
763        eprintln!("lean-ctx: {e}");
764        std::process::exit(1);
765    }
766}
767
768fn passthrough(command: &str) -> ! {
769    let (shell, flag) = shell::shell_and_flag();
770    let status = std::process::Command::new(&shell)
771        .arg(&flag)
772        .arg(command)
773        .env("LEAN_CTX_ACTIVE", "1")
774        .status()
775        .map(|s| s.code().unwrap_or(1))
776        .unwrap_or(127);
777    std::process::exit(status);
778}
779
780fn run_async<F: std::future::Future>(future: F) -> F::Output {
781    tokio::runtime::Runtime::new()
782        .expect("failed to create async runtime")
783        .block_on(future)
784}
785
786fn run_mcp_server() -> Result<()> {
787    use rmcp::ServiceExt;
788    use tracing_subscriber::EnvFilter;
789
790    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
791
792    let rt = tokio::runtime::Runtime::new()?;
793    rt.block_on(async {
794        tracing_subscriber::fmt()
795            .with_env_filter(EnvFilter::from_default_env())
796            .with_writer(std::io::stderr)
797            .init();
798
799        tracing::info!(
800            "lean-ctx v{} MCP server starting",
801            env!("CARGO_PKG_VERSION")
802        );
803
804        let server = tools::create_server();
805        let transport =
806            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
807        let service = server.serve(transport).await?;
808        service.waiting().await?;
809
810        core::stats::flush();
811        core::mode_predictor::ModePredictor::flush();
812        core::feedback::FeedbackStore::flush();
813
814        Ok(())
815    })
816}
817
818fn print_help() {
819    println!(
820        "lean-ctx {version} — Context Runtime for AI Agents
821
82290+ compression patterns | 46 MCP tools | Context Continuity Protocol
823
824USAGE:
825    lean-ctx                       Start MCP server (stdio)
826    lean-ctx serve                 Start MCP server (Streamable HTTP)
827    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
828    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
829    lean-ctx -c --raw \"command\"    Execute without compression (full output)
830    lean-ctx exec \"command\"        Same as -c
831    lean-ctx shell                 Interactive shell with compression
832
833COMMANDS:
834    gain                           Visual dashboard (colors, bars, sparklines, USD)
835    gain --live                    Live mode: auto-refreshes every 1s in-place
836    gain --graph                   30-day savings chart
837    gain --daily                   Bordered day-by-day table with USD
838    gain --json                    Raw JSON export of all stats
839         token-report [--json]          Token + memory report (project + session + CEP)
840    cep                            CEP impact report (score trends, cache, modes)
841    watch                          Live TUI dashboard (real-time event stream)
842    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
843    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
844    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
845    proxy status                   Show proxy statistics
846    cache [list|clear|stats]       Show/manage file read cache
847    wrapped [--week|--month|--all] Savings report card (shareable)
848    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
849    benchmark run [path] [--json]  Run real benchmark on project files
850    benchmark report [path]        Generate shareable Markdown report
851    cheatsheet                     Command cheat sheet & workflow quick reference
852    setup                          One-command setup: shell + editor + verify
853    bootstrap                      Non-interactive setup + fix (zero-config)
854    status [--json]                Show setup + MCP + rules status
855    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
856    init --agent <name>            Configure MCP for specific editor/agent
857    read <file> [-m mode]          Read file with compression
858    diff <file1> <file2>           Compressed file diff
859    grep <pattern> [path]          Search with compressed output
860    find <pattern> [path]          Find files with compressed output
861    ls [path]                      Directory listing with compression
862    deps [path]                    Show project dependencies
863    discover                       Find uncompressed commands in shell history
864    ghost [--json]                 Ghost Token report: find hidden token waste
865    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
866    session                        Show adoption statistics
867    config                         Show/edit configuration (~/.lean-ctx/config.toml)
868    theme [list|set|export|import] Customize terminal colors and themes
869    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
870    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
871    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
872    update [--check]               Self-update lean-ctx binary from GitHub Releases
873    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
874    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
875    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
876    uninstall                      Remove shell hook, MCP configs, and data directory
877
878SHELL HOOK PATTERNS (90+):
879    git       status, log, diff, add, commit, push, pull, fetch, clone,
880              branch, checkout, switch, merge, stash, tag, reset, remote
881    docker    build, ps, images, logs, compose, exec, network
882    npm/pnpm  install, test, run, list, outdated, audit
883    cargo     build, test, check, clippy
884    gh        pr list/view/create, issue list/view, run list/view
885    kubectl   get pods/services/deployments, logs, describe, apply
886    python    pip install/list/outdated, ruff check/format, poetry, uv
887    linters   eslint, biome, prettier, golangci-lint
888    builds    tsc, next build, vite build
889    ruby      rubocop, bundle install/update, rake test, rails test
890    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
891    iac       terraform, make, maven, gradle, dotnet, flutter, dart
892    utils     curl, grep/rg, find, ls, wget, env
893    data      JSON schema extraction, log deduplication
894
895READ MODES:
896    auto                           Auto-select optimal mode (default)
897    full                           Full content (cached re-reads = 13 tokens)
898    map                            Dependency graph + API signatures
899    signatures                     tree-sitter AST extraction (18 languages)
900    task                           Task-relevant filtering (requires ctx_session task)
901    reference                      One-line reference stub (cheap cache key)
902    aggressive                     Syntax-stripped content
903    entropy                        Shannon entropy filtered
904    diff                           Changed lines only
905    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
906
907ENVIRONMENT:
908    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
909    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
910    LEAN_CTX_RAW=1                 Same as --raw for current command
911    LEAN_CTX_AUTONOMY=false        Disable autonomous features
912    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
913
914OPTIONS:
915    --version, -V                  Show version
916    --help, -h                     Show this help
917
918EXAMPLES:
919    lean-ctx -c \"git status\"       Compressed git output
920    lean-ctx -c \"kubectl get pods\" Compressed k8s output
921    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
922    lean-ctx gain                  Visual terminal dashboard
923    lean-ctx gain --live           Live auto-updating terminal dashboard
924    lean-ctx gain --graph          30-day savings chart
925    lean-ctx gain --daily          Day-by-day breakdown with USD
926         lean-ctx token-report --json   Machine-readable token + memory report
927    lean-ctx dashboard             Open web dashboard at localhost:3333
928    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
929    lean-ctx wrapped               Weekly savings report card
930    lean-ctx wrapped --month       Monthly savings report card
931    lean-ctx sessions list         List all CCP sessions
932    lean-ctx sessions show         Show latest session state
933    lean-ctx discover              Find missed savings in shell history
934    lean-ctx setup                 One-command setup (shell + editors + verify)
935    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
936    lean-ctx bootstrap --json      Machine-readable bootstrap report
937    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
938    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
939    lean-ctx-off                   Disable all shell aliases
940    lean-ctx-mode track            Track mode: full output, stats recorded (default)
941    lean-ctx-mode compress         Compress mode: all output compressed (power users)
942    lean-ctx-mode off              Same as lean-ctx-off
943    lean-ctx-status                Show whether compression is active
944    lean-ctx init --agent pi       Install Pi Coding Agent extension
945    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
946    lean-ctx doctor --fix --json   Repair + machine-readable report
947    lean-ctx status --json         Machine-readable current status
948    lean-ctx read src/main.rs -m map
949    lean-ctx grep \"pub fn\" src/
950    lean-ctx deps .
951
952CLOUD:
953    cloud status                   Show cloud connection status
954    login <email>                  Log into existing LeanCTX Cloud account
955    register <email>               Create a new LeanCTX Cloud account
956    forgot-password <email>        Send password reset email
957    sync                           Upload local stats to cloud dashboard
958    contribute                     Share anonymized compression data
959
960TROUBLESHOOTING:
961    Commands broken?     lean-ctx-off             (fixes current session)
962    Permanent fix?       lean-ctx uninstall       (removes all hooks)
963    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
964    Binary missing?      Aliases auto-fallback to original commands (safe)
965    Preview init?        lean-ctx init --global --dry-run
966
967WEBSITE: https://leanctx.com
968GITHUB:  https://github.com/yvgude/lean-ctx
969",
970        version = env!("CARGO_PKG_VERSION"),
971    );
972}