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            "team" => {
214                let sub = rest.first().map_or("help", std::string::String::as_str);
215                match sub {
216                    "serve" => {
217                        #[cfg(feature = "team-server")]
218                        {
219                            let cfg_path = rest
220                                .iter()
221                                .enumerate()
222                                .find_map(|(i, a)| {
223                                    if let Some(v) = a.strip_prefix("--config=") {
224                                        return Some(v.to_string());
225                                    }
226                                    if a == "--config" {
227                                        return rest.get(i + 1).cloned();
228                                    }
229                                    None
230                                })
231                                .unwrap_or_default();
232
233                            if cfg_path.trim().is_empty() {
234                                eprintln!("Usage: lean-ctx team serve --config <path>");
235                                std::process::exit(1);
236                            }
237
238                            let cfg = crate::http_server::team::TeamServerConfig::load(
239                                std::path::Path::new(&cfg_path),
240                            )
241                            .unwrap_or_else(|e| {
242                                eprintln!("Invalid team config: {e}");
243                                std::process::exit(1);
244                            });
245
246                            if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
247                                tracing::error!("Team server error: {e}");
248                                std::process::exit(1);
249                            }
250                            return;
251                        }
252                        #[cfg(not(feature = "team-server"))]
253                        {
254                            eprintln!("lean-ctx team serve is not available in this build");
255                            std::process::exit(1);
256                        }
257                    }
258                    "token" => {
259                        let action = rest.get(1).map_or("help", std::string::String::as_str);
260                        if action == "create" {
261                            #[cfg(feature = "team-server")]
262                            {
263                                let args = &rest[2..];
264                                let cfg_path = args
265                                    .iter()
266                                    .enumerate()
267                                    .find_map(|(i, a)| {
268                                        if let Some(v) = a.strip_prefix("--config=") {
269                                            return Some(v.to_string());
270                                        }
271                                        if a == "--config" {
272                                            return args.get(i + 1).cloned();
273                                        }
274                                        None
275                                    })
276                                    .unwrap_or_default();
277                                let token_id = args
278                                    .iter()
279                                    .enumerate()
280                                    .find_map(|(i, a)| {
281                                        if let Some(v) = a.strip_prefix("--id=") {
282                                            return Some(v.to_string());
283                                        }
284                                        if a == "--id" {
285                                            return args.get(i + 1).cloned();
286                                        }
287                                        None
288                                    })
289                                    .unwrap_or_default();
290                                let scopes_csv = args
291                                    .iter()
292                                    .enumerate()
293                                    .find_map(|(i, a)| {
294                                        if let Some(v) = a.strip_prefix("--scopes=") {
295                                            return Some(v.to_string());
296                                        }
297                                        if let Some(v) = a.strip_prefix("--scope=") {
298                                            return Some(v.to_string());
299                                        }
300                                        if a == "--scopes" || a == "--scope" {
301                                            return args.get(i + 1).cloned();
302                                        }
303                                        None
304                                    })
305                                    .unwrap_or_default();
306
307                                if cfg_path.trim().is_empty()
308                                    || token_id.trim().is_empty()
309                                    || scopes_csv.trim().is_empty()
310                                {
311                                    eprintln!(
312                                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
313                                        );
314                                    std::process::exit(1);
315                                }
316
317                                let cfg_p = std::path::PathBuf::from(&cfg_path);
318                                let mut cfg = crate::http_server::team::TeamServerConfig::load(
319                                    cfg_p.as_path(),
320                                )
321                                .unwrap_or_else(|e| {
322                                    eprintln!("Invalid team config: {e}");
323                                    std::process::exit(1);
324                                });
325
326                                let mut scopes = Vec::new();
327                                for part in scopes_csv.split(',') {
328                                    let p = part.trim().to_ascii_lowercase();
329                                    if p.is_empty() {
330                                        continue;
331                                    }
332                                    let scope = match p.as_str() {
333                                        "search" => crate::http_server::team::TeamScope::Search,
334                                        "graph" => crate::http_server::team::TeamScope::Graph,
335                                        "artifacts" => {
336                                            crate::http_server::team::TeamScope::Artifacts
337                                        }
338                                        "index" => crate::http_server::team::TeamScope::Index,
339                                        _ => {
340                                            eprintln!("Unknown scope: {p}");
341                                            std::process::exit(1);
342                                        }
343                                    };
344                                    if !scopes.contains(&scope) {
345                                        scopes.push(scope);
346                                    }
347                                }
348                                if scopes.is_empty() {
349                                    eprintln!("At least 1 scope is required");
350                                    std::process::exit(1);
351                                }
352
353                                let (token, hash) = crate::http_server::team::create_token()
354                                    .unwrap_or_else(|e| {
355                                        eprintln!("Token generation failed: {e}");
356                                        std::process::exit(1);
357                                    });
358
359                                cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
360                                    id: token_id,
361                                    sha256_hex: hash,
362                                    scopes,
363                                });
364
365                                cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
366                                    eprintln!("Failed to write config: {e}");
367                                    std::process::exit(1);
368                                });
369
370                                println!("{token}");
371                                return;
372                            }
373
374                            #[cfg(not(feature = "team-server"))]
375                            {
376                                eprintln!("lean-ctx team token is not available in this build");
377                                std::process::exit(1);
378                            }
379                        }
380                        eprintln!(
381                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
382                        );
383                        std::process::exit(1);
384                    }
385                    "sync" => {
386                        #[cfg(feature = "team-server")]
387                        {
388                            let args = &rest[1..];
389                            let cfg_path = args
390                                .iter()
391                                .enumerate()
392                                .find_map(|(i, a)| {
393                                    if let Some(v) = a.strip_prefix("--config=") {
394                                        return Some(v.to_string());
395                                    }
396                                    if a == "--config" {
397                                        return args.get(i + 1).cloned();
398                                    }
399                                    None
400                                })
401                                .unwrap_or_default();
402                            if cfg_path.trim().is_empty() {
403                                eprintln!(
404                                    "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
405                                );
406                                std::process::exit(1);
407                            }
408                            let only_ws = args.iter().enumerate().find_map(|(i, a)| {
409                                if let Some(v) = a.strip_prefix("--workspace=") {
410                                    return Some(v.to_string());
411                                }
412                                if let Some(v) = a.strip_prefix("--workspace-id=") {
413                                    return Some(v.to_string());
414                                }
415                                if a == "--workspace" || a == "--workspace-id" {
416                                    return args.get(i + 1).cloned();
417                                }
418                                None
419                            });
420
421                            let cfg = crate::http_server::team::TeamServerConfig::load(
422                                std::path::Path::new(&cfg_path),
423                            )
424                            .unwrap_or_else(|e| {
425                                eprintln!("Invalid team config: {e}");
426                                std::process::exit(1);
427                            });
428
429                            for ws in &cfg.workspaces {
430                                if let Some(ref only) = only_ws {
431                                    if ws.id != *only {
432                                        continue;
433                                    }
434                                }
435                                let git_dir = ws.root.join(".git");
436                                if !git_dir.exists() {
437                                    eprintln!(
438                                        "workspace '{}' root is not a git repo: {}",
439                                        ws.id,
440                                        ws.root.display()
441                                    );
442                                    std::process::exit(1);
443                                }
444                                let status = std::process::Command::new("git")
445                                    .arg("-C")
446                                    .arg(&ws.root)
447                                    .args(["fetch", "--all", "--prune"])
448                                    .status()
449                                    .unwrap_or_else(|e| {
450                                        eprintln!(
451                                            "git fetch failed for workspace '{}': {e}",
452                                            ws.id
453                                        );
454                                        std::process::exit(1);
455                                    });
456                                if !status.success() {
457                                    eprintln!(
458                                        "git fetch failed for workspace '{}' (exit={})",
459                                        ws.id,
460                                        status.code().unwrap_or(1)
461                                    );
462                                    std::process::exit(1);
463                                }
464                            }
465                            return;
466                        }
467                        #[cfg(not(feature = "team-server"))]
468                        {
469                            eprintln!("lean-ctx team sync is not available in this build");
470                            std::process::exit(1);
471                        }
472                    }
473                    _ => {
474                        eprintln!(
475                            "Usage:\n  lean-ctx team serve --config <path>\n  lean-ctx team token create --config <path> --id <id> --scopes <csv>\n  lean-ctx team sync --config <path> [--workspace <id>]"
476                        );
477                        std::process::exit(1);
478                    }
479                }
480            }
481            "serve" => {
482                #[cfg(feature = "http-server")]
483                {
484                    let mut cfg = crate::http_server::HttpServerConfig::default();
485                    let mut i = 0;
486                    while i < rest.len() {
487                        match rest[i].as_str() {
488                            "--host" | "-H" => {
489                                i += 1;
490                                if i < rest.len() {
491                                    cfg.host.clone_from(&rest[i]);
492                                }
493                            }
494                            arg if arg.starts_with("--host=") => {
495                                cfg.host = arg["--host=".len()..].to_string();
496                            }
497                            "--port" | "-p" => {
498                                i += 1;
499                                if i < rest.len() {
500                                    if let Ok(p) = rest[i].parse::<u16>() {
501                                        cfg.port = p;
502                                    }
503                                }
504                            }
505                            arg if arg.starts_with("--port=") => {
506                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
507                                    cfg.port = p;
508                                }
509                            }
510                            "--project-root" => {
511                                i += 1;
512                                if i < rest.len() {
513                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
514                                }
515                            }
516                            arg if arg.starts_with("--project-root=") => {
517                                cfg.project_root =
518                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
519                            }
520                            "--auth-token" => {
521                                i += 1;
522                                if i < rest.len() {
523                                    cfg.auth_token = Some(rest[i].clone());
524                                }
525                            }
526                            arg if arg.starts_with("--auth-token=") => {
527                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
528                            }
529                            "--stateful" => cfg.stateful_mode = true,
530                            "--stateless" => cfg.stateful_mode = false,
531                            "--json" => cfg.json_response = true,
532                            "--sse" => cfg.json_response = false,
533                            "--disable-host-check" => cfg.disable_host_check = true,
534                            "--allowed-host" => {
535                                i += 1;
536                                if i < rest.len() {
537                                    cfg.allowed_hosts.push(rest[i].clone());
538                                }
539                            }
540                            arg if arg.starts_with("--allowed-host=") => {
541                                cfg.allowed_hosts
542                                    .push(arg["--allowed-host=".len()..].to_string());
543                            }
544                            "--max-body-bytes" => {
545                                i += 1;
546                                if i < rest.len() {
547                                    if let Ok(n) = rest[i].parse::<usize>() {
548                                        cfg.max_body_bytes = n;
549                                    }
550                                }
551                            }
552                            arg if arg.starts_with("--max-body-bytes=") => {
553                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
554                                    cfg.max_body_bytes = n;
555                                }
556                            }
557                            "--max-concurrency" => {
558                                i += 1;
559                                if i < rest.len() {
560                                    if let Ok(n) = rest[i].parse::<usize>() {
561                                        cfg.max_concurrency = n;
562                                    }
563                                }
564                            }
565                            arg if arg.starts_with("--max-concurrency=") => {
566                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
567                                    cfg.max_concurrency = n;
568                                }
569                            }
570                            "--max-rps" => {
571                                i += 1;
572                                if i < rest.len() {
573                                    if let Ok(n) = rest[i].parse::<u32>() {
574                                        cfg.max_rps = n;
575                                    }
576                                }
577                            }
578                            arg if arg.starts_with("--max-rps=") => {
579                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
580                                    cfg.max_rps = n;
581                                }
582                            }
583                            "--rate-burst" => {
584                                i += 1;
585                                if i < rest.len() {
586                                    if let Ok(n) = rest[i].parse::<u32>() {
587                                        cfg.rate_burst = n;
588                                    }
589                                }
590                            }
591                            arg if arg.starts_with("--rate-burst=") => {
592                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
593                                    cfg.rate_burst = n;
594                                }
595                            }
596                            "--request-timeout-ms" => {
597                                i += 1;
598                                if i < rest.len() {
599                                    if let Ok(n) = rest[i].parse::<u64>() {
600                                        cfg.request_timeout_ms = n;
601                                    }
602                                }
603                            }
604                            arg if arg.starts_with("--request-timeout-ms=") => {
605                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
606                                    cfg.request_timeout_ms = n;
607                                }
608                            }
609                            "--help" | "-h" => {
610                                eprintln!(
611                                    "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR]\\n\\
612                                     \\n\\
613                                     Options:\\n\\
614                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
615                                       --port, -p            Bind port (default: 8080)\\n\\
616                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
617                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
618                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
619                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
620                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
621                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
622                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
623                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
624                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
625                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
626                                       --disable-host-check  Disable Host header validation (unsafe)"
627                                );
628                                return;
629                            }
630                            _ => {}
631                        }
632                        i += 1;
633                    }
634
635                    if cfg.auth_token.is_none() {
636                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
637                            if !v.trim().is_empty() {
638                                cfg.auth_token = Some(v);
639                            }
640                        }
641                    }
642
643                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
644                        tracing::error!("HTTP server error: {e}");
645                        std::process::exit(1);
646                    }
647                    return;
648                }
649                #[cfg(not(feature = "http-server"))]
650                {
651                    eprintln!("lean-ctx serve is not available in this build");
652                    std::process::exit(1);
653                }
654            }
655            "watch" => {
656                if let Err(e) = tui::run() {
657                    tracing::error!("TUI error: {e}");
658                    std::process::exit(1);
659                }
660                return;
661            }
662            "proxy" => {
663                #[cfg(feature = "http-server")]
664                {
665                    let sub = rest.first().map_or("help", std::string::String::as_str);
666                    match sub {
667                        "start" => {
668                            let port: u16 = rest
669                                .iter()
670                                .find_map(|p| {
671                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
672                                })
673                                .and_then(|p| p.parse().ok())
674                                .unwrap_or(4444);
675                            let autostart = rest.iter().any(|a| a == "--autostart");
676                            if autostart {
677                                crate::proxy_autostart::install(port, false);
678                                return;
679                            }
680                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
681                                tracing::error!("Proxy error: {e}");
682                                std::process::exit(1);
683                            }
684                        }
685                        "stop" => {
686                            match ureq::get(&format!(
687                                "http://127.0.0.1:{}/health",
688                                rest.iter()
689                                    .find_map(|p| p.strip_prefix("--port="))
690                                    .and_then(|p| p.parse::<u16>().ok())
691                                    .unwrap_or(4444)
692                            ))
693                            .call()
694                            {
695                                Ok(_) => {
696                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
697                                }
698                                Err(_) => {
699                                    println!("No proxy running on that port.");
700                                }
701                            }
702                        }
703                        "status" => {
704                            let port: u16 = rest
705                                .iter()
706                                .find_map(|p| p.strip_prefix("--port="))
707                                .and_then(|p| p.parse().ok())
708                                .unwrap_or(4444);
709                            if let Ok(resp) =
710                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
711                            {
712                                let body = resp.into_body().read_to_string().unwrap_or_default();
713                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
714                                    println!("lean-ctx proxy status:");
715                                    println!("  Requests:    {}", v["requests_total"]);
716                                    println!("  Compressed:  {}", v["requests_compressed"]);
717                                    println!("  Tokens saved: {}", v["tokens_saved"]);
718                                    println!(
719                                        "  Compression: {}%",
720                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
721                                    );
722                                } else {
723                                    println!("{body}");
724                                }
725                            } else {
726                                println!("No proxy running on port {port}.");
727                                println!("Start with: lean-ctx proxy start");
728                            }
729                        }
730                        _ => {
731                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
732                        }
733                    }
734                    return;
735                }
736                #[cfg(not(feature = "http-server"))]
737                {
738                    eprintln!("lean-ctx proxy is not available in this build");
739                    std::process::exit(1);
740                }
741            }
742            "init" => {
743                super::cmd_init(&rest);
744                return;
745            }
746            "setup" => {
747                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
748                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
749                let fix = rest.iter().any(|a| a == "--fix");
750                let json = rest.iter().any(|a| a == "--json");
751
752                if non_interactive || fix || json || yes {
753                    let opts = setup::SetupOptions {
754                        non_interactive,
755                        yes,
756                        fix,
757                        json,
758                    };
759                    match setup::run_setup_with_options(opts) {
760                        Ok(report) => {
761                            if json {
762                                println!(
763                                    "{}",
764                                    serde_json::to_string_pretty(&report)
765                                        .unwrap_or_else(|_| "{}".to_string())
766                                );
767                            }
768                            if !report.success {
769                                std::process::exit(1);
770                            }
771                        }
772                        Err(e) => {
773                            eprintln!("{e}");
774                            std::process::exit(1);
775                        }
776                    }
777                } else {
778                    setup::run_setup();
779                }
780                return;
781            }
782            "bootstrap" => {
783                let json = rest.iter().any(|a| a == "--json");
784                let opts = setup::SetupOptions {
785                    non_interactive: true,
786                    yes: true,
787                    fix: true,
788                    json,
789                };
790                match setup::run_setup_with_options(opts) {
791                    Ok(report) => {
792                        if json {
793                            println!(
794                                "{}",
795                                serde_json::to_string_pretty(&report)
796                                    .unwrap_or_else(|_| "{}".to_string())
797                            );
798                        }
799                        if !report.success {
800                            std::process::exit(1);
801                        }
802                    }
803                    Err(e) => {
804                        eprintln!("{e}");
805                        std::process::exit(1);
806                    }
807                }
808                return;
809            }
810            "status" => {
811                let code = status::run_cli(&rest);
812                if code != 0 {
813                    std::process::exit(code);
814                }
815                return;
816            }
817            "read" => {
818                super::cmd_read(&rest);
819                return;
820            }
821            "diff" => {
822                super::cmd_diff(&rest);
823                return;
824            }
825            "grep" => {
826                super::cmd_grep(&rest);
827                return;
828            }
829            "find" => {
830                super::cmd_find(&rest);
831                return;
832            }
833            "ls" => {
834                super::cmd_ls(&rest);
835                return;
836            }
837            "deps" => {
838                super::cmd_deps(&rest);
839                return;
840            }
841            "discover" => {
842                super::cmd_discover(&rest);
843                return;
844            }
845            "ghost" => {
846                super::cmd_ghost(&rest);
847                return;
848            }
849            "filter" => {
850                super::cmd_filter(&rest);
851                return;
852            }
853            "heatmap" => {
854                heatmap::cmd_heatmap(&rest);
855                return;
856            }
857            "graph" => {
858                let sub = rest.first().map_or("build", std::string::String::as_str);
859                match sub {
860                    "build" => {
861                        let root = rest.get(1).cloned().or_else(|| {
862                            std::env::current_dir()
863                                .ok()
864                                .map(|p| p.to_string_lossy().to_string())
865                        });
866                        let root = root.unwrap_or_else(|| ".".to_string());
867                        let index = core::graph_index::load_or_build(&root);
868                        println!(
869                            "Graph built: {} files, {} edges",
870                            index.files.len(),
871                            index.edges.len()
872                        );
873                    }
874                    "export-html" => {
875                        let mut root: Option<String> = None;
876                        let mut out: Option<String> = None;
877                        let mut max_nodes: usize = 2500;
878
879                        let args = &rest[1..];
880                        let mut i = 0usize;
881                        while i < args.len() {
882                            let a = args[i].as_str();
883                            if let Some(v) = a.strip_prefix("--root=") {
884                                root = Some(v.to_string());
885                            } else if a == "--root" {
886                                root = args.get(i + 1).cloned();
887                                i += 1;
888                            } else if let Some(v) = a.strip_prefix("--out=") {
889                                out = Some(v.to_string());
890                            } else if a == "--out" {
891                                out = args.get(i + 1).cloned();
892                                i += 1;
893                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
894                                max_nodes = v.parse::<usize>().unwrap_or(0);
895                            } else if a == "--max-nodes" {
896                                let v = args.get(i + 1).map_or("", String::as_str);
897                                max_nodes = v.parse::<usize>().unwrap_or(0);
898                                i += 1;
899                            }
900                            i += 1;
901                        }
902
903                        let root = root
904                            .or_else(|| {
905                                std::env::current_dir()
906                                    .ok()
907                                    .map(|p| p.to_string_lossy().to_string())
908                            })
909                            .unwrap_or_else(|| ".".to_string());
910                        let Some(out) = out else {
911                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
912                            std::process::exit(1);
913                        };
914                        if max_nodes == 0 {
915                            eprintln!("--max-nodes must be >= 1");
916                            std::process::exit(1);
917                        }
918
919                        core::graph_export::export_graph_html(
920                            &root,
921                            std::path::Path::new(&out),
922                            max_nodes,
923                        )
924                        .unwrap_or_else(|e| {
925                            eprintln!("graph export failed: {e}");
926                            std::process::exit(1);
927                        });
928                        println!("{out}");
929                    }
930                    _ => {
931                        eprintln!(
932                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
933                        );
934                        std::process::exit(1);
935                    }
936                }
937                return;
938            }
939            "session" => {
940                super::cmd_session();
941                return;
942            }
943            "wrapped" => {
944                super::cmd_wrapped(&rest);
945                return;
946            }
947            "sessions" => {
948                super::cmd_sessions(&rest);
949                return;
950            }
951            "benchmark" => {
952                super::cmd_benchmark(&rest);
953                return;
954            }
955            "profile" => {
956                super::cmd_profile(&rest);
957                return;
958            }
959            "config" => {
960                super::cmd_config(&rest);
961                return;
962            }
963            "stats" => {
964                super::cmd_stats(&rest);
965                return;
966            }
967            "cache" => {
968                super::cmd_cache(&rest);
969                return;
970            }
971            "theme" => {
972                super::cmd_theme(&rest);
973                return;
974            }
975            "tee" => {
976                super::cmd_tee(&rest);
977                return;
978            }
979            "terse" => {
980                super::cmd_terse(&rest);
981                return;
982            }
983            "slow-log" => {
984                super::cmd_slow_log(&rest);
985                return;
986            }
987            "update" | "--self-update" => {
988                core::updater::run(&rest);
989                return;
990            }
991            "doctor" => {
992                let code = doctor::run_cli(&rest);
993                if code != 0 {
994                    std::process::exit(code);
995                }
996                return;
997            }
998            "gotchas" | "bugs" => {
999                super::cloud::cmd_gotchas(&rest);
1000                return;
1001            }
1002            "buddy" | "pet" => {
1003                super::cloud::cmd_buddy(&rest);
1004                return;
1005            }
1006            "hook" => {
1007                let action = rest.first().map_or("help", std::string::String::as_str);
1008                match action {
1009                    "rewrite" => hook_handlers::handle_rewrite(),
1010                    "redirect" => hook_handlers::handle_redirect(),
1011                    "copilot" => hook_handlers::handle_copilot(),
1012                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1013                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1014                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1015                    _ => {
1016                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1017                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1018                        std::process::exit(1);
1019                    }
1020                }
1021                return;
1022            }
1023            "report-issue" | "report" => {
1024                report::run(&rest);
1025                return;
1026            }
1027            "uninstall" => {
1028                let dry_run = rest.iter().any(|a| a == "--dry-run");
1029                uninstall::run(dry_run);
1030                return;
1031            }
1032            "bypass" => {
1033                if rest.is_empty() {
1034                    eprintln!("Usage: lean-ctx bypass \"command\"");
1035                    eprintln!("Runs the command with zero compression (raw passthrough).");
1036                    std::process::exit(1);
1037                }
1038                let command = if rest.len() == 1 {
1039                    rest[0].clone()
1040                } else {
1041                    shell::join_command(&args[2..])
1042                };
1043                std::env::set_var("LEAN_CTX_RAW", "1");
1044                let code = shell::exec(&command);
1045                std::process::exit(code);
1046            }
1047            "safety-levels" | "safety" => {
1048                println!("{}", core::compression_safety::format_safety_table());
1049                return;
1050            }
1051            "cheat" | "cheatsheet" | "cheat-sheet" => {
1052                super::cmd_cheatsheet();
1053                return;
1054            }
1055            "login" => {
1056                super::cloud::cmd_login(&rest);
1057                return;
1058            }
1059            "register" => {
1060                super::cloud::cmd_register(&rest);
1061                return;
1062            }
1063            "forgot-password" => {
1064                super::cloud::cmd_forgot_password(&rest);
1065                return;
1066            }
1067            "sync" => {
1068                super::cloud::cmd_sync();
1069                return;
1070            }
1071            "contribute" => {
1072                super::cloud::cmd_contribute();
1073                return;
1074            }
1075            "cloud" => {
1076                super::cloud::cmd_cloud(&rest);
1077                return;
1078            }
1079            "upgrade" => {
1080                super::cloud::cmd_upgrade();
1081                return;
1082            }
1083            "--version" | "-V" => {
1084                println!("{}", core::integrity::origin_line());
1085                return;
1086            }
1087            "--help" | "-h" => {
1088                print_help();
1089                return;
1090            }
1091            "mcp" => {}
1092            _ => {
1093                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1094                print_help();
1095                std::process::exit(1);
1096            }
1097        }
1098    }
1099
1100    if let Err(e) = run_mcp_server() {
1101        tracing::error!("lean-ctx: {e}");
1102        std::process::exit(1);
1103    }
1104}
1105
1106fn passthrough(command: &str) -> ! {
1107    let (shell, flag) = shell::shell_and_flag();
1108    let status = std::process::Command::new(&shell)
1109        .arg(&flag)
1110        .arg(command)
1111        .env("LEAN_CTX_ACTIVE", "1")
1112        .status()
1113        .map_or(127, |s| s.code().unwrap_or(1));
1114    std::process::exit(status);
1115}
1116
1117fn run_async<F: std::future::Future>(future: F) -> F::Output {
1118    tokio::runtime::Runtime::new()
1119        .expect("failed to create async runtime")
1120        .block_on(future)
1121}
1122
1123fn run_mcp_server() -> Result<()> {
1124    use rmcp::ServiceExt;
1125
1126    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1127
1128    // Concurrency hardening:
1129    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1130    // - Limits Tokio worker/blocking threads to avoid host degradation.
1131    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1132        "mcp-startup",
1133        std::time::Duration::from_secs(3),
1134        std::time::Duration::from_secs(30),
1135    );
1136
1137    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1138    let worker_threads = parallelism.clamp(1, 4);
1139    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1140
1141    let rt = tokio::runtime::Builder::new_multi_thread()
1142        .worker_threads(worker_threads)
1143        .max_blocking_threads(max_blocking_threads)
1144        .enable_all()
1145        .build()?;
1146
1147    let server = tools::create_server();
1148    drop(startup_lock);
1149
1150    rt.block_on(async {
1151        core::logging::init_mcp_logging();
1152
1153        tracing::info!(
1154            "lean-ctx v{} MCP server starting",
1155            env!("CARGO_PKG_VERSION")
1156        );
1157
1158        let transport =
1159            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1160        let service = match server.serve(transport).await {
1161            Ok(s) => s,
1162            Err(e) => {
1163                let msg = e.to_string();
1164                if msg.contains("expect initialized")
1165                    || msg.contains("context canceled")
1166                    || msg.contains("broken pipe")
1167                {
1168                    tracing::debug!("Client disconnected before init: {msg}");
1169                    return Ok(());
1170                }
1171                return Err(e.into());
1172            }
1173        };
1174        match service.waiting().await {
1175            Ok(reason) => {
1176                tracing::info!("MCP server stopped: {reason:?}");
1177            }
1178            Err(e) => {
1179                let msg = e.to_string();
1180                if msg.contains("broken pipe")
1181                    || msg.contains("connection reset")
1182                    || msg.contains("context canceled")
1183                {
1184                    tracing::info!("MCP server: transport closed ({msg})");
1185                } else {
1186                    tracing::error!("MCP server error: {msg}");
1187                }
1188            }
1189        }
1190
1191        core::stats::flush();
1192        core::mode_predictor::ModePredictor::flush();
1193        core::feedback::FeedbackStore::flush();
1194
1195        Ok(())
1196    })
1197}
1198
1199fn print_help() {
1200    println!(
1201        "lean-ctx {version} — Context Runtime for AI Agents
1202
120390+ compression patterns | 49 MCP tools | Context Continuity Protocol
1204
1205USAGE:
1206    lean-ctx                       Start MCP server (stdio)
1207    lean-ctx serve                 Start MCP server (Streamable HTTP)
1208    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1209    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1210    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1211    lean-ctx exec \"command\"        Same as -c
1212    lean-ctx shell                 Interactive shell with compression
1213
1214COMMANDS:
1215    gain                           Visual dashboard (colors, bars, sparklines, USD)
1216    gain --live                    Live mode: auto-refreshes every 1s in-place
1217    gain --graph                   30-day savings chart
1218    gain --daily                   Bordered day-by-day table with USD
1219    gain --json                    Raw JSON export of all stats
1220         token-report [--json]          Token + memory report (project + session + CEP)
1221    cep                            CEP impact report (score trends, cache, modes)
1222    watch                          Live TUI dashboard (real-time event stream)
1223    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1224    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1225    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1226    proxy status                   Show proxy statistics
1227    cache [list|clear|stats]       Show/manage file read cache
1228    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1229    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1230    benchmark run [path] [--json]  Run real benchmark on project files
1231    benchmark report [path]        Generate shareable Markdown report
1232    cheatsheet                     Command cheat sheet & workflow quick reference
1233    setup                          One-command setup: shell + editor + verify
1234    bootstrap                      Non-interactive setup + fix (zero-config)
1235    status [--json]                Show setup + MCP + rules status
1236    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1237    init --agent <name>            Configure MCP for specific editor/agent
1238    read <file> [-m mode]          Read file with compression
1239    diff <file1> <file2>           Compressed file diff
1240    grep <pattern> [path]          Search with compressed output
1241    find <pattern> [path]          Find files with compressed output
1242    ls [path]                      Directory listing with compression
1243    deps [path]                    Show project dependencies
1244    discover                       Find uncompressed commands in shell history
1245    ghost [--json]                 Ghost Token report: find hidden token waste
1246    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1247    session                        Show adoption statistics
1248    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1249    profile [list|show|diff|create|set]  Manage context profiles
1250    theme [list|set|export|import] Customize terminal colors and themes
1251    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1252    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1253    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1254    update [--check]               Self-update lean-ctx binary from GitHub Releases
1255    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1256    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1257    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1258    uninstall                      Remove shell hook, MCP configs, and data directory
1259
1260SHELL HOOK PATTERNS (90+):
1261    git       status, log, diff, add, commit, push, pull, fetch, clone,
1262              branch, checkout, switch, merge, stash, tag, reset, remote
1263    docker    build, ps, images, logs, compose, exec, network
1264    npm/pnpm  install, test, run, list, outdated, audit
1265    cargo     build, test, check, clippy
1266    gh        pr list/view/create, issue list/view, run list/view
1267    kubectl   get pods/services/deployments, logs, describe, apply
1268    python    pip install/list/outdated, ruff check/format, poetry, uv
1269    linters   eslint, biome, prettier, golangci-lint
1270    builds    tsc, next build, vite build
1271    ruby      rubocop, bundle install/update, rake test, rails test
1272    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1273    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1274    utils     curl, grep/rg, find, ls, wget, env
1275    data      JSON schema extraction, log deduplication
1276
1277READ MODES:
1278    auto                           Auto-select optimal mode (default)
1279    full                           Full content (cached re-reads = 13 tokens)
1280    map                            Dependency graph + API signatures
1281    signatures                     tree-sitter AST extraction (18 languages)
1282    task                           Task-relevant filtering (requires ctx_session task)
1283    reference                      One-line reference stub (cheap cache key)
1284    aggressive                     Syntax-stripped content
1285    entropy                        Shannon entropy filtered
1286    diff                           Changed lines only
1287    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1288
1289ENVIRONMENT:
1290    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1291    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1292    LEAN_CTX_RAW=1                 Same as --raw for current command
1293    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1294    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1295
1296OPTIONS:
1297    --version, -V                  Show version
1298    --help, -h                     Show this help
1299
1300EXAMPLES:
1301    lean-ctx -c \"git status\"       Compressed git output
1302    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1303    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1304    lean-ctx gain                  Visual terminal dashboard
1305    lean-ctx gain --live           Live auto-updating terminal dashboard
1306    lean-ctx gain --graph          30-day savings chart
1307    lean-ctx gain --daily          Day-by-day breakdown with USD
1308         lean-ctx token-report --json   Machine-readable token + memory report
1309    lean-ctx dashboard             Open web dashboard at localhost:3333
1310    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1311    lean-ctx gain --wrapped        Wrapped report card (recommended)
1312    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1313    lean-ctx sessions list         List all CCP sessions
1314    lean-ctx sessions show         Show latest session state
1315    lean-ctx discover              Find missed savings in shell history
1316    lean-ctx setup                 One-command setup (shell + editors + verify)
1317    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1318    lean-ctx bootstrap --json      Machine-readable bootstrap report
1319    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1320    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1321    lean-ctx-off                   Disable all shell aliases
1322    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1323    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1324    lean-ctx-mode off              Same as lean-ctx-off
1325    lean-ctx-status                Show whether compression is active
1326    lean-ctx init --agent pi       Install Pi Coding Agent extension
1327    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1328    lean-ctx doctor --fix --json   Repair + machine-readable report
1329    lean-ctx status --json         Machine-readable current status
1330    lean-ctx read src/main.rs -m map
1331    lean-ctx grep \"pub fn\" src/
1332    lean-ctx deps .
1333
1334CLOUD:
1335    cloud status                   Show cloud connection status
1336    login <email>                  Log into existing LeanCTX Cloud account
1337    register <email>               Create a new LeanCTX Cloud account
1338    forgot-password <email>        Send password reset email
1339    sync                           Upload local stats to cloud dashboard
1340    contribute                     Share anonymized compression data
1341
1342TROUBLESHOOTING:
1343    Commands broken?     lean-ctx-off             (fixes current session)
1344    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1345    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1346    Binary missing?      Aliases auto-fallback to original commands (safe)
1347    Preview init?        lean-ctx init --global --dry-run
1348
1349WEBSITE: https://leanctx.com
1350GITHUB:  https://github.com/yvgude/lean-ctx
1351",
1352        version = env!("CARGO_PKG_VERSION"),
1353    );
1354}