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" | "compression" => {
1146                super::cmd_compression(&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 export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1433    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1434    knowledge remove --category <c> --key <k>             Remove a fact
1435    knowledge status               Knowledge base summary
1436    overview [task]                Project overview (task-contextualized if given)
1437    compress [--signatures]        Context compression checkpoint
1438    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1439    profile [list|show|diff|create|set]  Manage context profiles
1440    theme [list|set|export|import] Customize terminal colors and themes
1441    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1442    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1443    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1444    update [--check]               Self-update lean-ctx binary from GitHub Releases
1445    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1446    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1447    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1448    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1449    uninstall                      Remove shell hook, MCP configs, and data directory
1450
1451SHELL HOOK PATTERNS (95+):
1452    git       status, log, diff, add, commit, push, pull, fetch, clone,
1453              branch, checkout, switch, merge, stash, tag, reset, remote
1454    docker    build, ps, images, logs, compose, exec, network
1455    npm/pnpm  install, test, run, list, outdated, audit
1456    cargo     build, test, check, clippy
1457    gh        pr list/view/create, issue list/view, run list/view
1458    kubectl   get pods/services/deployments, logs, describe, apply
1459    python    pip install/list/outdated, ruff check/format, poetry, uv
1460    linters   eslint, biome, prettier, golangci-lint
1461    builds    tsc, next build, vite build
1462    ruby      rubocop, bundle install/update, rake test, rails test
1463    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1464    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1465    utils     curl, grep/rg, find, ls, wget, env
1466    data      JSON schema extraction, log deduplication
1467
1468READ MODES:
1469    auto                           Auto-select optimal mode (default)
1470    full                           Full content (cached re-reads = 13 tokens)
1471    map                            Dependency graph + API signatures
1472    signatures                     tree-sitter AST extraction (18 languages)
1473    task                           Task-relevant filtering (requires ctx_session task)
1474    reference                      One-line reference stub (cheap cache key)
1475    aggressive                     Syntax-stripped content
1476    entropy                        Shannon entropy filtered
1477    diff                           Changed lines only
1478    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1479
1480ENVIRONMENT:
1481    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1482    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1483    LEAN_CTX_RAW=1                 Same as --raw for current command
1484    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1485    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1486
1487OPTIONS:
1488    --version, -V                  Show version
1489    --help, -h                     Show this help
1490
1491EXAMPLES:
1492    lean-ctx -c \"git status\"       Compressed git output
1493    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1494    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1495    lean-ctx gain                  Visual terminal dashboard
1496    lean-ctx gain --live           Live auto-updating terminal dashboard
1497    lean-ctx gain --graph          30-day savings chart
1498    lean-ctx gain --daily          Day-by-day breakdown with USD
1499         lean-ctx token-report --json   Machine-readable token + memory report
1500    lean-ctx dashboard             Open web dashboard at localhost:3333
1501    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1502    lean-ctx gain --wrapped        Wrapped report card (recommended)
1503    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1504    lean-ctx sessions list         List all CCP sessions
1505    lean-ctx sessions show         Show latest session state
1506    lean-ctx discover              Find missed savings in shell history
1507    lean-ctx setup                 One-command setup (shell + editors + verify)
1508    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1509    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1510    lean-ctx bootstrap --json      Machine-readable bootstrap report
1511    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1512    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1513    lean-ctx-off                   Disable all shell aliases
1514    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1515    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1516    lean-ctx-mode off              Same as lean-ctx-off
1517    lean-ctx-status                Show whether compression is active
1518    lean-ctx init --agent pi       Install Pi Coding Agent extension
1519    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1520    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1521    lean-ctx doctor --fix --json   Repair + machine-readable report
1522    lean-ctx status --json         Machine-readable current status
1523    lean-ctx session task \"implement auth\"
1524    lean-ctx session finding \"auth.rs:42 — missing validation\"
1525    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1526    lean-ctx knowledge recall \"authentication\"
1527    lean-ctx knowledge search \"database migration\"
1528    lean-ctx overview \"refactor auth module\"
1529    lean-ctx compress --signatures
1530    lean-ctx read src/main.rs -m map
1531    lean-ctx grep \"pub fn\" src/
1532    lean-ctx deps .
1533
1534CLOUD:
1535    cloud status                   Show cloud connection status
1536    login <email>                  Log into existing LeanCTX Cloud account
1537    register <email>               Create a new LeanCTX Cloud account
1538    forgot-password <email>        Send password reset email
1539    sync                           Upload local stats to cloud dashboard
1540    contribute                     Share anonymized compression data
1541
1542TROUBLESHOOTING:
1543    Commands broken?     lean-ctx-off             (fixes current session)
1544    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1545    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1546    Binary missing?      Aliases auto-fallback to original commands (safe)
1547    Preview init?        lean-ctx init --global --dry-run
1548
1549WEBSITE: https://leanctx.com
1550GITHUB:  https://github.com/yvgude/lean-ctx
1551",
1552        version = env!("CARGO_PKG_VERSION"),
1553    );
1554}