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().is_some_and(|a| a == "--raw");
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 code = if cmd_args.len() > 1 {
39                    shell::exec_argv(cmd_args)
40                } else {
41                    let command = cmd_args[0].clone();
42                    if std::env::var("LEAN_CTX_ACTIVE").is_ok()
43                        || std::env::var("LEAN_CTX_DISABLED").is_ok()
44                    {
45                        passthrough(&command);
46                    }
47                    shell::exec(&command)
48                };
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.clone_from(&rest[i]);
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                        tracing::error!("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                    tracing::error!("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_or("help", std::string::String::as_str);
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                                tracing::error!("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                            if let Ok(resp) =
442                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
443                            {
444                                let body = resp.into_body().read_to_string().unwrap_or_default();
445                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
446                                    println!("lean-ctx proxy status:");
447                                    println!("  Requests:    {}", v["requests_total"]);
448                                    println!("  Compressed:  {}", v["requests_compressed"]);
449                                    println!("  Tokens saved: {}", v["tokens_saved"]);
450                                    println!(
451                                        "  Compression: {}%",
452                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
453                                    );
454                                } else {
455                                    println!("{body}");
456                                }
457                            } else {
458                                println!("No proxy running on port {port}.");
459                                println!("Start with: lean-ctx proxy start");
460                            }
461                        }
462                        _ => {
463                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
464                        }
465                    }
466                    return;
467                }
468                #[cfg(not(feature = "http-server"))]
469                {
470                    eprintln!("lean-ctx proxy is not available in this build");
471                    std::process::exit(1);
472                }
473            }
474            "init" => {
475                super::cmd_init(&rest);
476                return;
477            }
478            "setup" => {
479                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
480                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
481                let fix = rest.iter().any(|a| a == "--fix");
482                let json = rest.iter().any(|a| a == "--json");
483
484                if non_interactive || fix || json || yes {
485                    let opts = setup::SetupOptions {
486                        non_interactive,
487                        yes,
488                        fix,
489                        json,
490                    };
491                    match setup::run_setup_with_options(opts) {
492                        Ok(report) => {
493                            if json {
494                                println!(
495                                    "{}",
496                                    serde_json::to_string_pretty(&report)
497                                        .unwrap_or_else(|_| "{}".to_string())
498                                );
499                            }
500                            if !report.success {
501                                std::process::exit(1);
502                            }
503                        }
504                        Err(e) => {
505                            eprintln!("{e}");
506                            std::process::exit(1);
507                        }
508                    }
509                } else {
510                    setup::run_setup();
511                }
512                return;
513            }
514            "bootstrap" => {
515                let json = rest.iter().any(|a| a == "--json");
516                let opts = setup::SetupOptions {
517                    non_interactive: true,
518                    yes: true,
519                    fix: true,
520                    json,
521                };
522                match setup::run_setup_with_options(opts) {
523                    Ok(report) => {
524                        if json {
525                            println!(
526                                "{}",
527                                serde_json::to_string_pretty(&report)
528                                    .unwrap_or_else(|_| "{}".to_string())
529                            );
530                        }
531                        if !report.success {
532                            std::process::exit(1);
533                        }
534                    }
535                    Err(e) => {
536                        eprintln!("{e}");
537                        std::process::exit(1);
538                    }
539                }
540                return;
541            }
542            "status" => {
543                let code = status::run_cli(&rest);
544                if code != 0 {
545                    std::process::exit(code);
546                }
547                return;
548            }
549            "read" => {
550                super::cmd_read(&rest);
551                return;
552            }
553            "diff" => {
554                super::cmd_diff(&rest);
555                return;
556            }
557            "grep" => {
558                super::cmd_grep(&rest);
559                return;
560            }
561            "find" => {
562                super::cmd_find(&rest);
563                return;
564            }
565            "ls" => {
566                super::cmd_ls(&rest);
567                return;
568            }
569            "deps" => {
570                super::cmd_deps(&rest);
571                return;
572            }
573            "discover" => {
574                super::cmd_discover(&rest);
575                return;
576            }
577            "ghost" => {
578                super::cmd_ghost(&rest);
579                return;
580            }
581            "filter" => {
582                super::cmd_filter(&rest);
583                return;
584            }
585            "heatmap" => {
586                heatmap::cmd_heatmap(&rest);
587                return;
588            }
589            "graph" => {
590                let mut action = "build";
591                let mut path_arg: Option<&str> = None;
592                for arg in &rest {
593                    if arg == "build" {
594                        action = "build";
595                    } else {
596                        path_arg = Some(arg.as_str());
597                    }
598                }
599                let root = path_arg
600                    .map(String::from)
601                    .or_else(|| {
602                        std::env::current_dir()
603                            .ok()
604                            .map(|p| p.to_string_lossy().to_string())
605                    })
606                    .unwrap_or_else(|| ".".to_string());
607                match action {
608                    "build" => {
609                        let index = core::graph_index::load_or_build(&root);
610                        println!(
611                            "Graph built: {} files, {} edges",
612                            index.files.len(),
613                            index.edges.len()
614                        );
615                    }
616                    _ => {
617                        eprintln!("Usage: lean-ctx graph [build] [path]");
618                    }
619                }
620                return;
621            }
622            "session" => {
623                super::cmd_session();
624                return;
625            }
626            "wrapped" => {
627                super::cmd_wrapped(&rest);
628                return;
629            }
630            "sessions" => {
631                super::cmd_sessions(&rest);
632                return;
633            }
634            "benchmark" => {
635                super::cmd_benchmark(&rest);
636                return;
637            }
638            "profile" => {
639                super::cmd_profile(&rest);
640                return;
641            }
642            "config" => {
643                super::cmd_config(&rest);
644                return;
645            }
646            "stats" => {
647                super::cmd_stats(&rest);
648                return;
649            }
650            "cache" => {
651                super::cmd_cache(&rest);
652                return;
653            }
654            "theme" => {
655                super::cmd_theme(&rest);
656                return;
657            }
658            "tee" => {
659                super::cmd_tee(&rest);
660                return;
661            }
662            "terse" => {
663                super::cmd_terse(&rest);
664                return;
665            }
666            "slow-log" => {
667                super::cmd_slow_log(&rest);
668                return;
669            }
670            "update" | "--self-update" => {
671                core::updater::run(&rest);
672                return;
673            }
674            "doctor" => {
675                let code = doctor::run_cli(&rest);
676                if code != 0 {
677                    std::process::exit(code);
678                }
679                return;
680            }
681            "gotchas" | "bugs" => {
682                super::cloud::cmd_gotchas(&rest);
683                return;
684            }
685            "buddy" | "pet" => {
686                super::cloud::cmd_buddy(&rest);
687                return;
688            }
689            "hook" => {
690                let action = rest.first().map_or("help", std::string::String::as_str);
691                match action {
692                    "rewrite" => hook_handlers::handle_rewrite(),
693                    "redirect" => hook_handlers::handle_redirect(),
694                    "copilot" => hook_handlers::handle_copilot(),
695                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
696                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
697                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
698                    _ => {
699                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
700                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
701                        std::process::exit(1);
702                    }
703                }
704                return;
705            }
706            "report-issue" | "report" => {
707                report::run(&rest);
708                return;
709            }
710            "uninstall" => {
711                let dry_run = rest.iter().any(|a| a == "--dry-run");
712                uninstall::run(dry_run);
713                return;
714            }
715            "bypass" => {
716                if rest.is_empty() {
717                    eprintln!("Usage: lean-ctx bypass \"command\"");
718                    eprintln!("Runs the command with zero compression (raw passthrough).");
719                    std::process::exit(1);
720                }
721                let command = if rest.len() == 1 {
722                    rest[0].clone()
723                } else {
724                    shell::join_command(&args[2..])
725                };
726                std::env::set_var("LEAN_CTX_RAW", "1");
727                let code = shell::exec(&command);
728                std::process::exit(code);
729            }
730            "safety-levels" | "safety" => {
731                println!("{}", core::compression_safety::format_safety_table());
732                return;
733            }
734            "cheat" | "cheatsheet" | "cheat-sheet" => {
735                super::cmd_cheatsheet();
736                return;
737            }
738            "login" => {
739                super::cloud::cmd_login(&rest);
740                return;
741            }
742            "register" => {
743                super::cloud::cmd_register(&rest);
744                return;
745            }
746            "forgot-password" => {
747                super::cloud::cmd_forgot_password(&rest);
748                return;
749            }
750            "sync" => {
751                super::cloud::cmd_sync();
752                return;
753            }
754            "contribute" => {
755                super::cloud::cmd_contribute();
756                return;
757            }
758            "cloud" => {
759                super::cloud::cmd_cloud(&rest);
760                return;
761            }
762            "upgrade" => {
763                super::cloud::cmd_upgrade();
764                return;
765            }
766            "--version" | "-V" => {
767                println!("{}", core::integrity::origin_line());
768                return;
769            }
770            "--help" | "-h" => {
771                print_help();
772                return;
773            }
774            "mcp" => {}
775            _ => {
776                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
777                print_help();
778                std::process::exit(1);
779            }
780        }
781    }
782
783    if let Err(e) = run_mcp_server() {
784        tracing::error!("lean-ctx: {e}");
785        std::process::exit(1);
786    }
787}
788
789fn passthrough(command: &str) -> ! {
790    let (shell, flag) = shell::shell_and_flag();
791    let status = std::process::Command::new(&shell)
792        .arg(&flag)
793        .arg(command)
794        .env("LEAN_CTX_ACTIVE", "1")
795        .status()
796        .map_or(127, |s| s.code().unwrap_or(1));
797    std::process::exit(status);
798}
799
800fn run_async<F: std::future::Future>(future: F) -> F::Output {
801    tokio::runtime::Runtime::new()
802        .expect("failed to create async runtime")
803        .block_on(future)
804}
805
806fn run_mcp_server() -> Result<()> {
807    use rmcp::ServiceExt;
808
809    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
810
811    // Concurrency hardening:
812    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
813    // - Limits Tokio worker/blocking threads to avoid host degradation.
814    let startup_lock = crate::core::startup_guard::try_acquire_lock(
815        "mcp-startup",
816        std::time::Duration::from_secs(3),
817        std::time::Duration::from_secs(30),
818    );
819
820    let parallelism = std::thread::available_parallelism()
821        .map(std::num::NonZeroUsize::get)
822        .unwrap_or(2);
823    let worker_threads = parallelism.clamp(1, 4);
824    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
825
826    let rt = tokio::runtime::Builder::new_multi_thread()
827        .worker_threads(worker_threads)
828        .max_blocking_threads(max_blocking_threads)
829        .enable_all()
830        .build()?;
831
832    let server = tools::create_server();
833    drop(startup_lock);
834
835    rt.block_on(async {
836        core::logging::init_mcp_logging();
837
838        tracing::info!(
839            "lean-ctx v{} MCP server starting",
840            env!("CARGO_PKG_VERSION")
841        );
842
843        let transport =
844            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
845        let service = match server.serve(transport).await {
846            Ok(s) => s,
847            Err(e) => {
848                let msg = e.to_string();
849                if msg.contains("expect initialized")
850                    || msg.contains("context canceled")
851                    || msg.contains("broken pipe")
852                {
853                    tracing::debug!("Client disconnected before init: {msg}");
854                    return Ok(());
855                }
856                return Err(e.into());
857            }
858        };
859        match service.waiting().await {
860            Ok(reason) => {
861                tracing::info!("MCP server stopped: {reason:?}");
862            }
863            Err(e) => {
864                let msg = e.to_string();
865                if msg.contains("broken pipe")
866                    || msg.contains("connection reset")
867                    || msg.contains("context canceled")
868                {
869                    tracing::info!("MCP server: transport closed ({msg})");
870                } else {
871                    tracing::error!("MCP server error: {msg}");
872                }
873            }
874        }
875
876        core::stats::flush();
877        core::mode_predictor::ModePredictor::flush();
878        core::feedback::FeedbackStore::flush();
879
880        Ok(())
881    })
882}
883
884fn print_help() {
885    println!(
886        "lean-ctx {version} — Context Runtime for AI Agents
887
88890+ compression patterns | 49 MCP tools | Context Continuity Protocol
889
890USAGE:
891    lean-ctx                       Start MCP server (stdio)
892    lean-ctx serve                 Start MCP server (Streamable HTTP)
893    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
894    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
895    lean-ctx -c --raw \"command\"    Execute without compression (full output)
896    lean-ctx exec \"command\"        Same as -c
897    lean-ctx shell                 Interactive shell with compression
898
899COMMANDS:
900    gain                           Visual dashboard (colors, bars, sparklines, USD)
901    gain --live                    Live mode: auto-refreshes every 1s in-place
902    gain --graph                   30-day savings chart
903    gain --daily                   Bordered day-by-day table with USD
904    gain --json                    Raw JSON export of all stats
905         token-report [--json]          Token + memory report (project + session + CEP)
906    cep                            CEP impact report (score trends, cache, modes)
907    watch                          Live TUI dashboard (real-time event stream)
908    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
909    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
910    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
911    proxy status                   Show proxy statistics
912    cache [list|clear|stats]       Show/manage file read cache
913    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
914    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
915    benchmark run [path] [--json]  Run real benchmark on project files
916    benchmark report [path]        Generate shareable Markdown report
917    cheatsheet                     Command cheat sheet & workflow quick reference
918    setup                          One-command setup: shell + editor + verify
919    bootstrap                      Non-interactive setup + fix (zero-config)
920    status [--json]                Show setup + MCP + rules status
921    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
922    init --agent <name>            Configure MCP for specific editor/agent
923    read <file> [-m mode]          Read file with compression
924    diff <file1> <file2>           Compressed file diff
925    grep <pattern> [path]          Search with compressed output
926    find <pattern> [path]          Find files with compressed output
927    ls [path]                      Directory listing with compression
928    deps [path]                    Show project dependencies
929    discover                       Find uncompressed commands in shell history
930    ghost [--json]                 Ghost Token report: find hidden token waste
931    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
932    session                        Show adoption statistics
933    config                         Show/edit configuration (~/.lean-ctx/config.toml)
934    profile [list|show|diff|create|set]  Manage context profiles
935    theme [list|set|export|import] Customize terminal colors and themes
936    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
937    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
938    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
939    update [--check]               Self-update lean-ctx binary from GitHub Releases
940    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
941    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
942    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
943    uninstall                      Remove shell hook, MCP configs, and data directory
944
945SHELL HOOK PATTERNS (90+):
946    git       status, log, diff, add, commit, push, pull, fetch, clone,
947              branch, checkout, switch, merge, stash, tag, reset, remote
948    docker    build, ps, images, logs, compose, exec, network
949    npm/pnpm  install, test, run, list, outdated, audit
950    cargo     build, test, check, clippy
951    gh        pr list/view/create, issue list/view, run list/view
952    kubectl   get pods/services/deployments, logs, describe, apply
953    python    pip install/list/outdated, ruff check/format, poetry, uv
954    linters   eslint, biome, prettier, golangci-lint
955    builds    tsc, next build, vite build
956    ruby      rubocop, bundle install/update, rake test, rails test
957    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
958    iac       terraform, make, maven, gradle, dotnet, flutter, dart
959    utils     curl, grep/rg, find, ls, wget, env
960    data      JSON schema extraction, log deduplication
961
962READ MODES:
963    auto                           Auto-select optimal mode (default)
964    full                           Full content (cached re-reads = 13 tokens)
965    map                            Dependency graph + API signatures
966    signatures                     tree-sitter AST extraction (18 languages)
967    task                           Task-relevant filtering (requires ctx_session task)
968    reference                      One-line reference stub (cheap cache key)
969    aggressive                     Syntax-stripped content
970    entropy                        Shannon entropy filtered
971    diff                           Changed lines only
972    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
973
974ENVIRONMENT:
975    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
976    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
977    LEAN_CTX_RAW=1                 Same as --raw for current command
978    LEAN_CTX_AUTONOMY=false        Disable autonomous features
979    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
980
981OPTIONS:
982    --version, -V                  Show version
983    --help, -h                     Show this help
984
985EXAMPLES:
986    lean-ctx -c \"git status\"       Compressed git output
987    lean-ctx -c \"kubectl get pods\" Compressed k8s output
988    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
989    lean-ctx gain                  Visual terminal dashboard
990    lean-ctx gain --live           Live auto-updating terminal dashboard
991    lean-ctx gain --graph          30-day savings chart
992    lean-ctx gain --daily          Day-by-day breakdown with USD
993         lean-ctx token-report --json   Machine-readable token + memory report
994    lean-ctx dashboard             Open web dashboard at localhost:3333
995    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
996    lean-ctx gain --wrapped        Wrapped report card (recommended)
997    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
998    lean-ctx sessions list         List all CCP sessions
999    lean-ctx sessions show         Show latest session state
1000    lean-ctx discover              Find missed savings in shell history
1001    lean-ctx setup                 One-command setup (shell + editors + verify)
1002    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1003    lean-ctx bootstrap --json      Machine-readable bootstrap report
1004    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1005    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1006    lean-ctx-off                   Disable all shell aliases
1007    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1008    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1009    lean-ctx-mode off              Same as lean-ctx-off
1010    lean-ctx-status                Show whether compression is active
1011    lean-ctx init --agent pi       Install Pi Coding Agent extension
1012    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1013    lean-ctx doctor --fix --json   Repair + machine-readable report
1014    lean-ctx status --json         Machine-readable current status
1015    lean-ctx read src/main.rs -m map
1016    lean-ctx grep \"pub fn\" src/
1017    lean-ctx deps .
1018
1019CLOUD:
1020    cloud status                   Show cloud connection status
1021    login <email>                  Log into existing LeanCTX Cloud account
1022    register <email>               Create a new LeanCTX Cloud account
1023    forgot-password <email>        Send password reset email
1024    sync                           Upload local stats to cloud dashboard
1025    contribute                     Share anonymized compression data
1026
1027TROUBLESHOOTING:
1028    Commands broken?     lean-ctx-off             (fixes current session)
1029    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1030    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1031    Binary missing?      Aliases auto-fallback to original commands (safe)
1032    Preview init?        lean-ctx init --global --dry-run
1033
1034WEBSITE: https://leanctx.com
1035GITHUB:  https://github.com/yvgude/lean-ctx
1036",
1037        version = env!("CARGO_PKG_VERSION"),
1038    );
1039}