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