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                return;
956            }
957            "diff" => {
958                super::cmd_diff(&rest);
959                return;
960            }
961            "grep" => {
962                super::cmd_grep(&rest);
963                return;
964            }
965            "find" => {
966                super::cmd_find(&rest);
967                return;
968            }
969            "ls" => {
970                super::cmd_ls(&rest);
971                return;
972            }
973            "deps" => {
974                super::cmd_deps(&rest);
975                return;
976            }
977            "discover" => {
978                super::cmd_discover(&rest);
979                return;
980            }
981            "ghost" => {
982                super::cmd_ghost(&rest);
983                return;
984            }
985            "filter" => {
986                super::cmd_filter(&rest);
987                return;
988            }
989            "heatmap" => {
990                heatmap::cmd_heatmap(&rest);
991                return;
992            }
993            "graph" => {
994                let sub = rest.first().map_or("build", std::string::String::as_str);
995                match sub {
996                    "build" => {
997                        let root = rest.get(1).cloned().or_else(|| {
998                            std::env::current_dir()
999                                .ok()
1000                                .map(|p| p.to_string_lossy().to_string())
1001                        });
1002                        let root = root.unwrap_or_else(|| ".".to_string());
1003                        let index = core::graph_index::load_or_build(&root);
1004                        println!(
1005                            "Graph built: {} files, {} edges",
1006                            index.files.len(),
1007                            index.edges.len()
1008                        );
1009                    }
1010                    "export-html" => {
1011                        let mut root: Option<String> = None;
1012                        let mut out: Option<String> = None;
1013                        let mut max_nodes: usize = 2500;
1014
1015                        let args = &rest[1..];
1016                        let mut i = 0usize;
1017                        while i < args.len() {
1018                            let a = args[i].as_str();
1019                            if let Some(v) = a.strip_prefix("--root=") {
1020                                root = Some(v.to_string());
1021                            } else if a == "--root" {
1022                                root = args.get(i + 1).cloned();
1023                                i += 1;
1024                            } else if let Some(v) = a.strip_prefix("--out=") {
1025                                out = Some(v.to_string());
1026                            } else if a == "--out" {
1027                                out = args.get(i + 1).cloned();
1028                                i += 1;
1029                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1030                                max_nodes = v.parse::<usize>().unwrap_or(0);
1031                            } else if a == "--max-nodes" {
1032                                let v = args.get(i + 1).map_or("", String::as_str);
1033                                max_nodes = v.parse::<usize>().unwrap_or(0);
1034                                i += 1;
1035                            }
1036                            i += 1;
1037                        }
1038
1039                        let root = root
1040                            .or_else(|| {
1041                                std::env::current_dir()
1042                                    .ok()
1043                                    .map(|p| p.to_string_lossy().to_string())
1044                            })
1045                            .unwrap_or_else(|| ".".to_string());
1046                        let Some(out) = out else {
1047                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1048                            std::process::exit(1);
1049                        };
1050                        if max_nodes == 0 {
1051                            eprintln!("--max-nodes must be >= 1");
1052                            std::process::exit(1);
1053                        }
1054
1055                        core::graph_export::export_graph_html(
1056                            &root,
1057                            std::path::Path::new(&out),
1058                            max_nodes,
1059                        )
1060                        .unwrap_or_else(|e| {
1061                            eprintln!("graph export failed: {e}");
1062                            std::process::exit(1);
1063                        });
1064                        println!("{out}");
1065                    }
1066                    _ => {
1067                        eprintln!(
1068                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1069                        );
1070                        std::process::exit(1);
1071                    }
1072                }
1073                return;
1074            }
1075            "session" => {
1076                super::cmd_session_action(&rest);
1077                return;
1078            }
1079            "control" | "context-control" => {
1080                super::cmd_control(&rest);
1081                return;
1082            }
1083            "plan" | "context-plan" => {
1084                super::cmd_plan(&rest);
1085                return;
1086            }
1087            "compile" | "context-compile" => {
1088                super::cmd_compile(&rest);
1089                return;
1090            }
1091            "knowledge" => {
1092                super::cmd_knowledge(&rest);
1093                return;
1094            }
1095            "overview" => {
1096                super::cmd_overview(&rest);
1097                return;
1098            }
1099            "compress" => {
1100                super::cmd_compress(&rest);
1101                return;
1102            }
1103            "wrapped" => {
1104                super::cmd_wrapped(&rest);
1105                return;
1106            }
1107            "sessions" => {
1108                super::cmd_sessions(&rest);
1109                return;
1110            }
1111            "benchmark" => {
1112                super::cmd_benchmark(&rest);
1113                return;
1114            }
1115            "profile" => {
1116                super::cmd_profile(&rest);
1117                return;
1118            }
1119            "config" => {
1120                super::cmd_config(&rest);
1121                return;
1122            }
1123            "stats" => {
1124                super::cmd_stats(&rest);
1125                return;
1126            }
1127            "cache" => {
1128                super::cmd_cache(&rest);
1129                return;
1130            }
1131            "theme" => {
1132                super::cmd_theme(&rest);
1133                return;
1134            }
1135            "tee" => {
1136                super::cmd_tee(&rest);
1137                return;
1138            }
1139            "terse" => {
1140                super::cmd_terse(&rest);
1141                return;
1142            }
1143            "slow-log" => {
1144                super::cmd_slow_log(&rest);
1145                return;
1146            }
1147            "update" | "--self-update" => {
1148                core::updater::run(&rest);
1149                return;
1150            }
1151            "doctor" => {
1152                let code = doctor::run_cli(&rest);
1153                if code != 0 {
1154                    std::process::exit(code);
1155                }
1156                return;
1157            }
1158            "gotchas" | "bugs" => {
1159                super::cloud::cmd_gotchas(&rest);
1160                return;
1161            }
1162            "buddy" | "pet" => {
1163                super::cloud::cmd_buddy(&rest);
1164                return;
1165            }
1166            "hook" => {
1167                hook_handlers::mark_hook_environment();
1168                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1169                let action = rest.first().map_or("help", std::string::String::as_str);
1170                match action {
1171                    "rewrite" => hook_handlers::handle_rewrite(),
1172                    "redirect" => hook_handlers::handle_redirect(),
1173                    "copilot" => hook_handlers::handle_copilot(),
1174                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1175                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1176                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1177                    _ => {
1178                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1179                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1180                        std::process::exit(1);
1181                    }
1182                }
1183                return;
1184            }
1185            "report-issue" | "report" => {
1186                report::run(&rest);
1187                return;
1188            }
1189            "uninstall" => {
1190                let dry_run = rest.iter().any(|a| a == "--dry-run");
1191                uninstall::run(dry_run);
1192                return;
1193            }
1194            "bypass" => {
1195                if rest.is_empty() {
1196                    eprintln!("Usage: lean-ctx bypass \"command\"");
1197                    eprintln!("Runs the command with zero compression (raw passthrough).");
1198                    std::process::exit(1);
1199                }
1200                let command = if rest.len() == 1 {
1201                    rest[0].clone()
1202                } else {
1203                    shell::join_command(&args[2..])
1204                };
1205                std::env::set_var("LEAN_CTX_RAW", "1");
1206                let code = shell::exec(&command);
1207                std::process::exit(code);
1208            }
1209            "safety-levels" | "safety" => {
1210                println!("{}", core::compression_safety::format_safety_table());
1211                return;
1212            }
1213            "cheat" | "cheatsheet" | "cheat-sheet" => {
1214                super::cmd_cheatsheet();
1215                return;
1216            }
1217            "login" => {
1218                super::cloud::cmd_login(&rest);
1219                return;
1220            }
1221            "register" => {
1222                super::cloud::cmd_register(&rest);
1223                return;
1224            }
1225            "forgot-password" => {
1226                super::cloud::cmd_forgot_password(&rest);
1227                return;
1228            }
1229            "sync" => {
1230                super::cloud::cmd_sync();
1231                return;
1232            }
1233            "contribute" => {
1234                super::cloud::cmd_contribute();
1235                return;
1236            }
1237            "cloud" => {
1238                super::cloud::cmd_cloud(&rest);
1239                return;
1240            }
1241            "upgrade" => {
1242                super::cloud::cmd_upgrade();
1243                return;
1244            }
1245            "--version" | "-V" => {
1246                println!("{}", core::integrity::origin_line());
1247                return;
1248            }
1249            "--help" | "-h" => {
1250                print_help();
1251                return;
1252            }
1253            "mcp" => {}
1254            _ => {
1255                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1256                print_help();
1257                std::process::exit(1);
1258            }
1259        }
1260    }
1261
1262    if let Err(e) = run_mcp_server() {
1263        tracing::error!("lean-ctx: {e}");
1264        std::process::exit(1);
1265    }
1266}
1267
1268fn passthrough(command: &str) -> ! {
1269    let (shell, flag) = shell::shell_and_flag();
1270    let status = std::process::Command::new(&shell)
1271        .arg(&flag)
1272        .arg(command)
1273        .env("LEAN_CTX_ACTIVE", "1")
1274        .status()
1275        .map_or(127, |s| s.code().unwrap_or(1));
1276    std::process::exit(status);
1277}
1278
1279fn run_async<F: std::future::Future>(future: F) -> F::Output {
1280    tokio::runtime::Runtime::new()
1281        .expect("failed to create async runtime")
1282        .block_on(future)
1283}
1284
1285fn run_mcp_server() -> Result<()> {
1286    use rmcp::ServiceExt;
1287
1288    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1289
1290    // Concurrency hardening:
1291    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1292    // - Limits Tokio worker/blocking threads to avoid host degradation.
1293    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1294        "mcp-startup",
1295        std::time::Duration::from_secs(3),
1296        std::time::Duration::from_secs(30),
1297    );
1298
1299    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1300    let worker_threads = parallelism.clamp(1, 4);
1301    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1302
1303    let rt = tokio::runtime::Builder::new_multi_thread()
1304        .worker_threads(worker_threads)
1305        .max_blocking_threads(max_blocking_threads)
1306        .enable_all()
1307        .build()?;
1308
1309    let server = tools::create_server();
1310    drop(startup_lock);
1311
1312    rt.block_on(async {
1313        core::logging::init_mcp_logging();
1314
1315        tracing::info!(
1316            "lean-ctx v{} MCP server starting",
1317            env!("CARGO_PKG_VERSION")
1318        );
1319
1320        let transport =
1321            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1322        let service = match server.serve(transport).await {
1323            Ok(s) => s,
1324            Err(e) => {
1325                let msg = e.to_string();
1326                if msg.contains("expect initialized")
1327                    || msg.contains("context canceled")
1328                    || msg.contains("broken pipe")
1329                {
1330                    tracing::debug!("Client disconnected before init: {msg}");
1331                    return Ok(());
1332                }
1333                return Err(e.into());
1334            }
1335        };
1336        match service.waiting().await {
1337            Ok(reason) => {
1338                tracing::info!("MCP server stopped: {reason:?}");
1339            }
1340            Err(e) => {
1341                let msg = e.to_string();
1342                if msg.contains("broken pipe")
1343                    || msg.contains("connection reset")
1344                    || msg.contains("context canceled")
1345                {
1346                    tracing::info!("MCP server: transport closed ({msg})");
1347                } else {
1348                    tracing::error!("MCP server error: {msg}");
1349                }
1350            }
1351        }
1352
1353        core::stats::flush();
1354        core::heatmap::flush();
1355        core::mode_predictor::ModePredictor::flush();
1356        core::feedback::FeedbackStore::flush();
1357
1358        Ok(())
1359    })
1360}
1361
1362fn print_help() {
1363    println!(
1364        "lean-ctx {version} — Context Runtime for AI Agents
1365
136695+ compression patterns | 57 MCP tools | Context Continuity Protocol
1367
1368USAGE:
1369    lean-ctx                       Start MCP server (stdio)
1370    lean-ctx serve                 Start MCP server (Streamable HTTP)
1371    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1372    lean-ctx serve --stop          Stop running daemon
1373    lean-ctx serve --status        Show daemon status
1374    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1375    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1376    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1377    lean-ctx exec \"command\"        Same as -c
1378    lean-ctx shell                 Interactive shell with compression
1379
1380COMMANDS:
1381    gain                           Visual dashboard (colors, bars, sparklines, USD)
1382    gain --live                    Live mode: auto-refreshes every 1s in-place
1383    gain --graph                   30-day savings chart
1384    gain --daily                   Bordered day-by-day table with USD
1385    gain --json                    Raw JSON export of all stats
1386         token-report [--json]          Token + memory report (project + session + CEP)
1387    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1388    index <status|build|build-full|watch>  Codebase index utilities
1389    cep                            CEP impact report (score trends, cache, modes)
1390    watch                          Live TUI dashboard (real-time event stream)
1391    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1392    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1393    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1394    proxy status                   Show proxy statistics
1395    cache [list|clear|stats]       Show/manage file read cache
1396    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1397    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1398    benchmark run [path] [--json]  Run real benchmark on project files
1399    benchmark report [path]        Generate shareable Markdown report
1400    cheatsheet                     Command cheat sheet & workflow quick reference
1401    setup                          One-command setup: shell + editor + verify
1402    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1403    bootstrap                      Non-interactive setup + fix (zero-config)
1404    status [--json]                Show setup + MCP + rules status
1405    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1406    init --agent <name>            Configure MCP for specific editor/agent
1407    read <file> [-m mode]          Read file with compression
1408    diff <file1> <file2>           Compressed file diff
1409    grep <pattern> [path]          Search with compressed output
1410    find <pattern> [path]          Find files with compressed output
1411    ls [path]                      Directory listing with compression
1412    deps [path]                    Show project dependencies
1413    discover                       Find uncompressed commands in shell history
1414    ghost [--json]                 Ghost Token report: find hidden token waste
1415    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1416    session                        Show adoption statistics
1417    session task <desc>            Set current task
1418    session finding <summary>      Record a finding
1419    session save                   Save current session
1420    session load [id]              Load session (latest if no ID)
1421    knowledge remember <value> --category <c> --key <k>   Store a fact
1422    knowledge recall [query] [--category <c>]             Retrieve facts
1423    knowledge search <query>       Cross-project knowledge search
1424    knowledge status               Knowledge base summary
1425    overview [task]                Project overview (task-contextualized if given)
1426    compress [--signatures]        Context compression checkpoint
1427    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1428    profile [list|show|diff|create|set]  Manage context profiles
1429    theme [list|set|export|import] Customize terminal colors and themes
1430    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1431    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1432    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1433    update [--check]               Self-update lean-ctx binary from GitHub Releases
1434    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1435    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1436    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1437    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1438    uninstall                      Remove shell hook, MCP configs, and data directory
1439
1440SHELL HOOK PATTERNS (95+):
1441    git       status, log, diff, add, commit, push, pull, fetch, clone,
1442              branch, checkout, switch, merge, stash, tag, reset, remote
1443    docker    build, ps, images, logs, compose, exec, network
1444    npm/pnpm  install, test, run, list, outdated, audit
1445    cargo     build, test, check, clippy
1446    gh        pr list/view/create, issue list/view, run list/view
1447    kubectl   get pods/services/deployments, logs, describe, apply
1448    python    pip install/list/outdated, ruff check/format, poetry, uv
1449    linters   eslint, biome, prettier, golangci-lint
1450    builds    tsc, next build, vite build
1451    ruby      rubocop, bundle install/update, rake test, rails test
1452    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1453    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1454    utils     curl, grep/rg, find, ls, wget, env
1455    data      JSON schema extraction, log deduplication
1456
1457READ MODES:
1458    auto                           Auto-select optimal mode (default)
1459    full                           Full content (cached re-reads = 13 tokens)
1460    map                            Dependency graph + API signatures
1461    signatures                     tree-sitter AST extraction (18 languages)
1462    task                           Task-relevant filtering (requires ctx_session task)
1463    reference                      One-line reference stub (cheap cache key)
1464    aggressive                     Syntax-stripped content
1465    entropy                        Shannon entropy filtered
1466    diff                           Changed lines only
1467    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1468
1469ENVIRONMENT:
1470    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1471    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1472    LEAN_CTX_RAW=1                 Same as --raw for current command
1473    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1474    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1475
1476OPTIONS:
1477    --version, -V                  Show version
1478    --help, -h                     Show this help
1479
1480EXAMPLES:
1481    lean-ctx -c \"git status\"       Compressed git output
1482    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1483    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1484    lean-ctx gain                  Visual terminal dashboard
1485    lean-ctx gain --live           Live auto-updating terminal dashboard
1486    lean-ctx gain --graph          30-day savings chart
1487    lean-ctx gain --daily          Day-by-day breakdown with USD
1488         lean-ctx token-report --json   Machine-readable token + memory report
1489    lean-ctx dashboard             Open web dashboard at localhost:3333
1490    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1491    lean-ctx gain --wrapped        Wrapped report card (recommended)
1492    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1493    lean-ctx sessions list         List all CCP sessions
1494    lean-ctx sessions show         Show latest session state
1495    lean-ctx discover              Find missed savings in shell history
1496    lean-ctx setup                 One-command setup (shell + editors + verify)
1497    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1498    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1499    lean-ctx bootstrap --json      Machine-readable bootstrap report
1500    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1501    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1502    lean-ctx-off                   Disable all shell aliases
1503    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1504    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1505    lean-ctx-mode off              Same as lean-ctx-off
1506    lean-ctx-status                Show whether compression is active
1507    lean-ctx init --agent pi       Install Pi Coding Agent extension
1508    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1509    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1510    lean-ctx doctor --fix --json   Repair + machine-readable report
1511    lean-ctx status --json         Machine-readable current status
1512    lean-ctx session task \"implement auth\"
1513    lean-ctx session finding \"auth.rs:42 — missing validation\"
1514    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1515    lean-ctx knowledge recall \"authentication\"
1516    lean-ctx knowledge search \"database migration\"
1517    lean-ctx overview \"refactor auth module\"
1518    lean-ctx compress --signatures
1519    lean-ctx read src/main.rs -m map
1520    lean-ctx grep \"pub fn\" src/
1521    lean-ctx deps .
1522
1523CLOUD:
1524    cloud status                   Show cloud connection status
1525    login <email>                  Log into existing LeanCTX Cloud account
1526    register <email>               Create a new LeanCTX Cloud account
1527    forgot-password <email>        Send password reset email
1528    sync                           Upload local stats to cloud dashboard
1529    contribute                     Share anonymized compression data
1530
1531TROUBLESHOOTING:
1532    Commands broken?     lean-ctx-off             (fixes current session)
1533    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1534    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1535    Binary missing?      Aliases auto-fallback to original commands (safe)
1536    Preview init?        lean-ctx init --global --dry-run
1537
1538WEBSITE: https://leanctx.com
1539GITHUB:  https://github.com/yvgude/lean-ctx
1540",
1541        version = env!("CARGO_PKG_VERSION"),
1542    );
1543}