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                    if stop_mode {
685                        if let Err(e) = crate::daemon::stop_daemon() {
686                            eprintln!("Error: {e}");
687                            std::process::exit(1);
688                        }
689                        return;
690                    }
691
692                    if status_mode {
693                        println!("{}", crate::daemon::daemon_status());
694                        return;
695                    }
696
697                    if daemon_mode {
698                        if let Err(e) = crate::daemon::start_daemon(&rest) {
699                            eprintln!("Error: {e}");
700                            std::process::exit(1);
701                        }
702                        return;
703                    }
704
705                    if foreground_daemon {
706                        if let Err(e) = crate::daemon::init_foreground_daemon() {
707                            eprintln!("Error writing PID file: {e}");
708                            std::process::exit(1);
709                        }
710                        let addr = crate::daemon::daemon_addr();
711                        if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
712                        {
713                            tracing::error!("Daemon server error: {e}");
714                            crate::daemon::cleanup_daemon_files();
715                            std::process::exit(1);
716                        }
717                        crate::daemon::cleanup_daemon_files();
718                        return;
719                    }
720
721                    if cfg.auth_token.is_none() {
722                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
723                            if !v.trim().is_empty() {
724                                cfg.auth_token = Some(v);
725                            }
726                        }
727                    }
728
729                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
730                        tracing::error!("HTTP server error: {e}");
731                        std::process::exit(1);
732                    }
733                    return;
734                }
735                #[cfg(not(feature = "http-server"))]
736                {
737                    eprintln!("lean-ctx serve is not available in this build");
738                    std::process::exit(1);
739                }
740            }
741            "watch" => {
742                if rest.iter().any(|a| a == "--help" || a == "-h") {
743                    println!("Usage: lean-ctx watch");
744                    println!("  Live TUI dashboard (real-time event stream).");
745                    return;
746                }
747                if let Err(e) = tui::run() {
748                    tracing::error!("TUI error: {e}");
749                    std::process::exit(1);
750                }
751                return;
752            }
753            "proxy" => {
754                #[cfg(feature = "http-server")]
755                {
756                    let sub = rest.first().map_or("help", std::string::String::as_str);
757                    match sub {
758                        "start" => {
759                            let port: u16 = rest
760                                .iter()
761                                .find_map(|p| {
762                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
763                                })
764                                .and_then(|p| p.parse().ok())
765                                .unwrap_or(4444);
766                            let autostart = rest.iter().any(|a| a == "--autostart");
767                            if autostart {
768                                crate::proxy_autostart::install(port, false);
769                                return;
770                            }
771                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
772                                tracing::error!("Proxy error: {e}");
773                                std::process::exit(1);
774                            }
775                        }
776                        "stop" => {
777                            match ureq::get(&format!(
778                                "http://127.0.0.1:{}/health",
779                                rest.iter()
780                                    .find_map(|p| p.strip_prefix("--port="))
781                                    .and_then(|p| p.parse::<u16>().ok())
782                                    .unwrap_or(4444)
783                            ))
784                            .call()
785                            {
786                                Ok(_) => {
787                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
788                                }
789                                Err(_) => {
790                                    println!("No proxy running on that port.");
791                                }
792                            }
793                        }
794                        "status" => {
795                            let port: u16 = rest
796                                .iter()
797                                .find_map(|p| p.strip_prefix("--port="))
798                                .and_then(|p| p.parse().ok())
799                                .unwrap_or(4444);
800                            if let Ok(resp) =
801                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
802                            {
803                                let body = resp.into_body().read_to_string().unwrap_or_default();
804                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
805                                    println!("lean-ctx proxy status:");
806                                    println!("  Requests:    {}", v["requests_total"]);
807                                    println!("  Compressed:  {}", v["requests_compressed"]);
808                                    println!("  Tokens saved: {}", v["tokens_saved"]);
809                                    println!(
810                                        "  Compression: {}%",
811                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
812                                    );
813                                } else {
814                                    println!("{body}");
815                                }
816                            } else {
817                                println!("No proxy running on port {port}.");
818                                println!("Start with: lean-ctx proxy start");
819                            }
820                        }
821                        _ => {
822                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
823                        }
824                    }
825                    return;
826                }
827                #[cfg(not(feature = "http-server"))]
828                {
829                    eprintln!("lean-ctx proxy is not available in this build");
830                    std::process::exit(1);
831                }
832            }
833            "init" => {
834                super::cmd_init(&rest);
835                return;
836            }
837            "setup" => {
838                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
839                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
840                let fix = rest.iter().any(|a| a == "--fix");
841                let json = rest.iter().any(|a| a == "--json");
842                let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
843
844                if non_interactive || fix || json || yes {
845                    let opts = setup::SetupOptions {
846                        non_interactive,
847                        yes,
848                        fix,
849                        json,
850                        no_auto_approve,
851                    };
852                    match setup::run_setup_with_options(opts) {
853                        Ok(report) => {
854                            if json {
855                                println!(
856                                    "{}",
857                                    serde_json::to_string_pretty(&report)
858                                        .unwrap_or_else(|_| "{}".to_string())
859                                );
860                            }
861                            if !report.success {
862                                std::process::exit(1);
863                            }
864                        }
865                        Err(e) => {
866                            eprintln!("{e}");
867                            std::process::exit(1);
868                        }
869                    }
870                } else {
871                    setup::run_setup();
872                }
873                return;
874            }
875            "install" => {
876                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
877                let json = rest.iter().any(|a| a == "--json");
878                if !repair {
879                    eprintln!("Usage: lean-ctx install --repair [--json]");
880                    std::process::exit(1);
881                }
882                let opts = setup::SetupOptions {
883                    non_interactive: true,
884                    yes: true,
885                    fix: true,
886                    json,
887                    ..Default::default()
888                };
889                match setup::run_setup_with_options(opts) {
890                    Ok(report) => {
891                        if json {
892                            println!(
893                                "{}",
894                                serde_json::to_string_pretty(&report)
895                                    .unwrap_or_else(|_| "{}".to_string())
896                            );
897                        }
898                        if !report.success {
899                            std::process::exit(1);
900                        }
901                    }
902                    Err(e) => {
903                        eprintln!("{e}");
904                        std::process::exit(1);
905                    }
906                }
907                return;
908            }
909            "bootstrap" => {
910                let json = rest.iter().any(|a| a == "--json");
911                let opts = setup::SetupOptions {
912                    non_interactive: true,
913                    yes: true,
914                    fix: true,
915                    json,
916                    ..Default::default()
917                };
918                match setup::run_setup_with_options(opts) {
919                    Ok(report) => {
920                        if json {
921                            println!(
922                                "{}",
923                                serde_json::to_string_pretty(&report)
924                                    .unwrap_or_else(|_| "{}".to_string())
925                            );
926                        }
927                        if !report.success {
928                            std::process::exit(1);
929                        }
930                    }
931                    Err(e) => {
932                        eprintln!("{e}");
933                        std::process::exit(1);
934                    }
935                }
936                return;
937            }
938            "status" => {
939                let code = status::run_cli(&rest);
940                if code != 0 {
941                    std::process::exit(code);
942                }
943                return;
944            }
945            "read" => {
946                super::cmd_read(&rest);
947                core::stats::flush();
948                return;
949            }
950            "diff" => {
951                super::cmd_diff(&rest);
952                core::stats::flush();
953                return;
954            }
955            "grep" => {
956                super::cmd_grep(&rest);
957                core::stats::flush();
958                return;
959            }
960            "find" => {
961                super::cmd_find(&rest);
962                core::stats::flush();
963                return;
964            }
965            "ls" => {
966                super::cmd_ls(&rest);
967                core::stats::flush();
968                return;
969            }
970            "deps" => {
971                super::cmd_deps(&rest);
972                core::stats::flush();
973                return;
974            }
975            "discover" => {
976                super::cmd_discover(&rest);
977                return;
978            }
979            "ghost" => {
980                super::cmd_ghost(&rest);
981                return;
982            }
983            "filter" => {
984                super::cmd_filter(&rest);
985                return;
986            }
987            "heatmap" => {
988                heatmap::cmd_heatmap(&rest);
989                return;
990            }
991            "graph" => {
992                let sub = rest.first().map_or("build", std::string::String::as_str);
993                match sub {
994                    "build" => {
995                        let root = rest.get(1).cloned().or_else(|| {
996                            std::env::current_dir()
997                                .ok()
998                                .map(|p| p.to_string_lossy().to_string())
999                        });
1000                        let root = root.unwrap_or_else(|| ".".to_string());
1001                        let index = core::graph_index::load_or_build(&root);
1002                        println!(
1003                            "Graph built: {} files, {} edges",
1004                            index.files.len(),
1005                            index.edges.len()
1006                        );
1007                    }
1008                    "export-html" => {
1009                        let mut root: Option<String> = None;
1010                        let mut out: Option<String> = None;
1011                        let mut max_nodes: usize = 2500;
1012
1013                        let args = &rest[1..];
1014                        let mut i = 0usize;
1015                        while i < args.len() {
1016                            let a = args[i].as_str();
1017                            if let Some(v) = a.strip_prefix("--root=") {
1018                                root = Some(v.to_string());
1019                            } else if a == "--root" {
1020                                root = args.get(i + 1).cloned();
1021                                i += 1;
1022                            } else if let Some(v) = a.strip_prefix("--out=") {
1023                                out = Some(v.to_string());
1024                            } else if a == "--out" {
1025                                out = args.get(i + 1).cloned();
1026                                i += 1;
1027                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1028                                max_nodes = v.parse::<usize>().unwrap_or(0);
1029                            } else if a == "--max-nodes" {
1030                                let v = args.get(i + 1).map_or("", String::as_str);
1031                                max_nodes = v.parse::<usize>().unwrap_or(0);
1032                                i += 1;
1033                            }
1034                            i += 1;
1035                        }
1036
1037                        let root = root
1038                            .or_else(|| {
1039                                std::env::current_dir()
1040                                    .ok()
1041                                    .map(|p| p.to_string_lossy().to_string())
1042                            })
1043                            .unwrap_or_else(|| ".".to_string());
1044                        let Some(out) = out else {
1045                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1046                            std::process::exit(1);
1047                        };
1048                        if max_nodes == 0 {
1049                            eprintln!("--max-nodes must be >= 1");
1050                            std::process::exit(1);
1051                        }
1052
1053                        core::graph_export::export_graph_html(
1054                            &root,
1055                            std::path::Path::new(&out),
1056                            max_nodes,
1057                        )
1058                        .unwrap_or_else(|e| {
1059                            eprintln!("graph export failed: {e}");
1060                            std::process::exit(1);
1061                        });
1062                        println!("{out}");
1063                    }
1064                    _ => {
1065                        eprintln!(
1066                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1067                        );
1068                        std::process::exit(1);
1069                    }
1070                }
1071                return;
1072            }
1073            "smells" => {
1074                let action = rest.first().map_or("summary", String::as_str);
1075                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1076                    if let Some(v) = a.strip_prefix("--rule=") {
1077                        return Some(v.to_string());
1078                    }
1079                    if a == "--rule" {
1080                        return rest.get(i + 1).cloned();
1081                    }
1082                    None
1083                });
1084                let path = rest.iter().enumerate().find_map(|(i, a)| {
1085                    if let Some(v) = a.strip_prefix("--path=") {
1086                        return Some(v.to_string());
1087                    }
1088                    if a == "--path" {
1089                        return rest.get(i + 1).cloned();
1090                    }
1091                    None
1092                });
1093                let root = rest
1094                    .iter()
1095                    .enumerate()
1096                    .find_map(|(i, a)| {
1097                        if let Some(v) = a.strip_prefix("--root=") {
1098                            return Some(v.to_string());
1099                        }
1100                        if a == "--root" {
1101                            return rest.get(i + 1).cloned();
1102                        }
1103                        None
1104                    })
1105                    .or_else(|| {
1106                        std::env::current_dir()
1107                            .ok()
1108                            .map(|p| p.to_string_lossy().to_string())
1109                    })
1110                    .unwrap_or_else(|| ".".to_string());
1111                let fmt = if rest.iter().any(|a| a == "--json") {
1112                    Some("json")
1113                } else {
1114                    None
1115                };
1116                println!(
1117                    "{}",
1118                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1119                );
1120                return;
1121            }
1122            "session" => {
1123                super::cmd_session_action(&rest);
1124                return;
1125            }
1126            "control" | "context-control" => {
1127                super::cmd_control(&rest);
1128                return;
1129            }
1130            "plan" | "context-plan" => {
1131                super::cmd_plan(&rest);
1132                return;
1133            }
1134            "compile" | "context-compile" => {
1135                super::cmd_compile(&rest);
1136                return;
1137            }
1138            "knowledge" => {
1139                super::cmd_knowledge(&rest);
1140                return;
1141            }
1142            "overview" => {
1143                super::cmd_overview(&rest);
1144                return;
1145            }
1146            "compress" => {
1147                super::cmd_compress(&rest);
1148                return;
1149            }
1150            "wrapped" => {
1151                super::cmd_wrapped(&rest);
1152                return;
1153            }
1154            "sessions" => {
1155                super::cmd_sessions(&rest);
1156                return;
1157            }
1158            "benchmark" => {
1159                super::cmd_benchmark(&rest);
1160                return;
1161            }
1162            "profile" => {
1163                super::cmd_profile(&rest);
1164                return;
1165            }
1166            "config" => {
1167                super::cmd_config(&rest);
1168                return;
1169            }
1170            "stats" => {
1171                super::cmd_stats(&rest);
1172                return;
1173            }
1174            "cache" => {
1175                super::cmd_cache(&rest);
1176                return;
1177            }
1178            "theme" => {
1179                super::cmd_theme(&rest);
1180                return;
1181            }
1182            "tee" => {
1183                super::cmd_tee(&rest);
1184                return;
1185            }
1186            "terse" | "compression" => {
1187                super::cmd_compression(&rest);
1188                return;
1189            }
1190            "slow-log" => {
1191                super::cmd_slow_log(&rest);
1192                return;
1193            }
1194            "update" | "--self-update" => {
1195                core::updater::run(&rest);
1196                return;
1197            }
1198            "doctor" => {
1199                let code = doctor::run_cli(&rest);
1200                if code != 0 {
1201                    std::process::exit(code);
1202                }
1203                return;
1204            }
1205            "gotchas" | "bugs" => {
1206                super::cloud::cmd_gotchas(&rest);
1207                return;
1208            }
1209            "learn" => {
1210                super::cmd_learn(&rest);
1211                return;
1212            }
1213            "buddy" | "pet" => {
1214                super::cloud::cmd_buddy(&rest);
1215                return;
1216            }
1217            "hook" => {
1218                hook_handlers::mark_hook_environment();
1219                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1220                let action = rest.first().map_or("help", std::string::String::as_str);
1221                match action {
1222                    "rewrite" => hook_handlers::handle_rewrite(),
1223                    "redirect" => hook_handlers::handle_redirect(),
1224                    "copilot" => hook_handlers::handle_copilot(),
1225                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1226                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1227                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1228                    _ => {
1229                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1230                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1231                        std::process::exit(1);
1232                    }
1233                }
1234                return;
1235            }
1236            "report-issue" | "report" => {
1237                report::run(&rest);
1238                return;
1239            }
1240            "uninstall" => {
1241                let dry_run = rest.iter().any(|a| a == "--dry-run");
1242                uninstall::run(dry_run);
1243                return;
1244            }
1245            "bypass" => {
1246                if rest.is_empty() {
1247                    eprintln!("Usage: lean-ctx bypass \"command\"");
1248                    eprintln!("Runs the command with zero compression (raw passthrough).");
1249                    std::process::exit(1);
1250                }
1251                let command = if rest.len() == 1 {
1252                    rest[0].clone()
1253                } else {
1254                    shell::join_command(&args[2..])
1255                };
1256                std::env::set_var("LEAN_CTX_RAW", "1");
1257                let code = shell::exec(&command);
1258                std::process::exit(code);
1259            }
1260            "safety-levels" | "safety" => {
1261                println!("{}", core::compression_safety::format_safety_table());
1262                return;
1263            }
1264            "cheat" | "cheatsheet" | "cheat-sheet" => {
1265                super::cmd_cheatsheet();
1266                return;
1267            }
1268            "login" => {
1269                super::cloud::cmd_login(&rest);
1270                return;
1271            }
1272            "register" => {
1273                super::cloud::cmd_register(&rest);
1274                return;
1275            }
1276            "forgot-password" => {
1277                super::cloud::cmd_forgot_password(&rest);
1278                return;
1279            }
1280            "sync" => {
1281                super::cloud::cmd_sync();
1282                return;
1283            }
1284            "contribute" => {
1285                super::cloud::cmd_contribute();
1286                return;
1287            }
1288            "cloud" => {
1289                super::cloud::cmd_cloud(&rest);
1290                return;
1291            }
1292            "upgrade" => {
1293                super::cloud::cmd_upgrade();
1294                return;
1295            }
1296            "--version" | "-V" => {
1297                println!("{}", core::integrity::origin_line());
1298                return;
1299            }
1300            "--help" | "-h" => {
1301                print_help();
1302                return;
1303            }
1304            "mcp" => {}
1305            _ => {
1306                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1307                print_help();
1308                std::process::exit(1);
1309            }
1310        }
1311    }
1312
1313    if let Err(e) = run_mcp_server() {
1314        tracing::error!("lean-ctx: {e}");
1315        std::process::exit(1);
1316    }
1317}
1318
1319fn passthrough(command: &str) -> ! {
1320    let (shell, flag) = shell::shell_and_flag();
1321    let status = std::process::Command::new(&shell)
1322        .arg(&flag)
1323        .arg(command)
1324        .env("LEAN_CTX_ACTIVE", "1")
1325        .status()
1326        .map_or(127, |s| s.code().unwrap_or(1));
1327    std::process::exit(status);
1328}
1329
1330fn run_async<F: std::future::Future>(future: F) -> F::Output {
1331    tokio::runtime::Runtime::new()
1332        .expect("failed to create async runtime")
1333        .block_on(future)
1334}
1335
1336fn run_mcp_server() -> Result<()> {
1337    use rmcp::ServiceExt;
1338
1339    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1340
1341    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1342
1343    // Concurrency hardening:
1344    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1345    // - Limits Tokio worker/blocking threads to avoid host degradation.
1346    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1347        "mcp-startup",
1348        std::time::Duration::from_secs(3),
1349        std::time::Duration::from_secs(30),
1350    );
1351
1352    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1353    let worker_threads = parallelism.clamp(1, 4);
1354    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1355
1356    let rt = tokio::runtime::Builder::new_multi_thread()
1357        .worker_threads(worker_threads)
1358        .max_blocking_threads(max_blocking_threads)
1359        .enable_all()
1360        .build()?;
1361
1362    let server = tools::create_server();
1363    drop(startup_lock);
1364
1365    rt.block_on(async {
1366        core::logging::init_mcp_logging();
1367        core::protocol::set_mcp_context(true);
1368
1369        tracing::info!(
1370            "lean-ctx v{} MCP server starting",
1371            env!("CARGO_PKG_VERSION")
1372        );
1373
1374        let transport =
1375            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1376        let server_handle = server.clone();
1377        let service = match server.serve(transport).await {
1378            Ok(s) => s,
1379            Err(e) => {
1380                let msg = e.to_string();
1381                if msg.contains("expect initialized")
1382                    || msg.contains("context canceled")
1383                    || msg.contains("broken pipe")
1384                {
1385                    tracing::debug!("Client disconnected before init: {msg}");
1386                    return Ok(());
1387                }
1388                return Err(e.into());
1389            }
1390        };
1391        match service.waiting().await {
1392            Ok(reason) => {
1393                tracing::info!("MCP server stopped: {reason:?}");
1394            }
1395            Err(e) => {
1396                let msg = e.to_string();
1397                if msg.contains("broken pipe")
1398                    || msg.contains("connection reset")
1399                    || msg.contains("context canceled")
1400                {
1401                    tracing::info!("MCP server: transport closed ({msg})");
1402                } else {
1403                    tracing::error!("MCP server error: {msg}");
1404                }
1405            }
1406        }
1407
1408        server_handle.shutdown().await;
1409
1410        core::stats::flush();
1411        core::heatmap::flush();
1412        core::mode_predictor::ModePredictor::flush();
1413        core::feedback::FeedbackStore::flush();
1414
1415        Ok(())
1416    })
1417}
1418
1419fn print_help() {
1420    println!(
1421        "lean-ctx {version} — Context Runtime for AI Agents
1422
142395+ compression patterns | 59 MCP tools | Context Continuity Protocol
1424
1425USAGE:
1426    lean-ctx                       Start MCP server (stdio)
1427    lean-ctx serve                 Start MCP server (Streamable HTTP)
1428    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1429    lean-ctx serve --stop          Stop running daemon
1430    lean-ctx serve --status        Show daemon status
1431    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1432    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1433    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1434    lean-ctx exec \"command\"        Same as -c
1435    lean-ctx shell                 Interactive shell with compression
1436
1437COMMANDS:
1438    gain                           Visual dashboard (colors, bars, sparklines, USD)
1439    gain --live                    Live mode: auto-refreshes every 1s in-place
1440    gain --graph                   30-day savings chart
1441    gain --daily                   Bordered day-by-day table with USD
1442    gain --json                    Raw JSON export of all stats
1443         token-report [--json]          Token + memory report (project + session + CEP)
1444    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1445    index <status|build|build-full|watch>  Codebase index utilities
1446    cep                            CEP impact report (score trends, cache, modes)
1447    watch                          Live TUI dashboard (real-time event stream)
1448    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1449    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1450    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1451    proxy status                   Show proxy statistics
1452    cache [list|clear|stats]       Show/manage file read cache
1453    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1454    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1455    benchmark run [path] [--json]  Run real benchmark on project files
1456    benchmark report [path]        Generate shareable Markdown report
1457    cheatsheet                     Command cheat sheet & workflow quick reference
1458    setup                          One-command setup: shell + editor + verify
1459    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1460    bootstrap                      Non-interactive setup + fix (zero-config)
1461    status [--json]                Show setup + MCP + rules status
1462    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1463    init --agent <name>            Configure MCP for specific editor/agent
1464    read <file> [-m mode]          Read file with compression
1465    diff <file1> <file2>           Compressed file diff
1466    grep <pattern> [path]          Search with compressed output
1467    find <pattern> [path]          Find files with compressed output
1468    ls [path]                      Directory listing with compression
1469    deps [path]                    Show project dependencies
1470    discover                       Find uncompressed commands in shell history
1471    ghost [--json]                 Ghost Token report: find hidden token waste
1472    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1473    session                        Show adoption statistics
1474    session task <desc>            Set current task
1475    session finding <summary>      Record a finding
1476    session save                   Save current session
1477    session load [id]              Load session (latest if no ID)
1478    knowledge remember <value> --category <c> --key <k>   Store a fact
1479    knowledge recall [query] [--category <c>]             Retrieve facts
1480    knowledge search <query>       Cross-project knowledge search
1481    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1482    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1483    knowledge remove --category <c> --key <k>             Remove a fact
1484    knowledge status               Knowledge base summary
1485    overview [task]                Project overview (task-contextualized if given)
1486    compress [--signatures]        Context compression checkpoint
1487    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1488    profile [list|show|diff|create|set]  Manage context profiles
1489    theme [list|set|export|import] Customize terminal colors and themes
1490    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1491    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1492    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1493    update [--check]               Self-update lean-ctx binary from GitHub Releases
1494    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1495    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1496    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1497    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1498    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1499                                   Code smell detection (Property Graph, 8 rules)
1500    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1501    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1502    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1503    uninstall                      Remove shell hook, MCP configs, and data directory
1504
1505SHELL HOOK PATTERNS (95+):
1506    git       status, log, diff, add, commit, push, pull, fetch, clone,
1507              branch, checkout, switch, merge, stash, tag, reset, remote
1508    docker    build, ps, images, logs, compose, exec, network
1509    npm/pnpm  install, test, run, list, outdated, audit
1510    cargo     build, test, check, clippy
1511    gh        pr list/view/create, issue list/view, run list/view
1512    kubectl   get pods/services/deployments, logs, describe, apply
1513    python    pip install/list/outdated, ruff check/format, poetry, uv
1514    linters   eslint, biome, prettier, golangci-lint
1515    builds    tsc, next build, vite build
1516    ruby      rubocop, bundle install/update, rake test, rails test
1517    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1518    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1519    utils     curl, grep/rg, find, ls, wget, env
1520    data      JSON schema extraction, log deduplication
1521
1522READ MODES:
1523    auto                           Auto-select optimal mode (default)
1524    full                           Full content (cached re-reads = 13 tokens)
1525    map                            Dependency graph + API signatures
1526    signatures                     tree-sitter AST extraction (18 languages)
1527    task                           Task-relevant filtering (requires ctx_session task)
1528    reference                      One-line reference stub (cheap cache key)
1529    aggressive                     Syntax-stripped content
1530    entropy                        Shannon entropy filtered
1531    diff                           Changed lines only
1532    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1533
1534ENVIRONMENT:
1535    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1536    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1537    LEAN_CTX_RAW=1                 Same as --raw for current command
1538    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1539    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1540
1541OPTIONS:
1542    --version, -V                  Show version
1543    --help, -h                     Show this help
1544
1545EXAMPLES:
1546    lean-ctx -c \"git status\"       Compressed git output
1547    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1548    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1549    lean-ctx gain                  Visual terminal dashboard
1550    lean-ctx gain --live           Live auto-updating terminal dashboard
1551    lean-ctx gain --graph          30-day savings chart
1552    lean-ctx gain --daily          Day-by-day breakdown with USD
1553         lean-ctx token-report --json   Machine-readable token + memory report
1554    lean-ctx dashboard             Open web dashboard at localhost:3333
1555    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1556    lean-ctx gain --wrapped        Wrapped report card (recommended)
1557    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1558    lean-ctx sessions list         List all CCP sessions
1559    lean-ctx sessions show         Show latest session state
1560    lean-ctx discover              Find missed savings in shell history
1561    lean-ctx setup                 One-command setup (shell + editors + verify)
1562    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1563    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1564    lean-ctx bootstrap --json      Machine-readable bootstrap report
1565    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1566    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1567    lean-ctx-off                   Disable all shell aliases
1568    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1569    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1570    lean-ctx-mode off              Same as lean-ctx-off
1571    lean-ctx-status                Show whether compression is active
1572    lean-ctx init --agent pi       Install Pi Coding Agent extension
1573    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1574    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1575    lean-ctx doctor --fix --json   Repair + machine-readable report
1576    lean-ctx status --json         Machine-readable current status
1577    lean-ctx session task \"implement auth\"
1578    lean-ctx session finding \"auth.rs:42 — missing validation\"
1579    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1580    lean-ctx knowledge recall \"authentication\"
1581    lean-ctx knowledge search \"database migration\"
1582    lean-ctx overview \"refactor auth module\"
1583    lean-ctx compress --signatures
1584    lean-ctx read src/main.rs -m map
1585    lean-ctx grep \"pub fn\" src/
1586    lean-ctx deps .
1587
1588CLOUD:
1589    cloud status                   Show cloud connection status
1590    login <email>                  Log into existing LeanCTX Cloud account
1591    register <email>               Create a new LeanCTX Cloud account
1592    forgot-password <email>        Send password reset email
1593    sync                           Upload local stats to cloud dashboard
1594    contribute                     Share anonymized compression data
1595
1596TROUBLESHOOTING:
1597    Commands broken?     lean-ctx-off             (fixes current session)
1598    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1599    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1600    Binary missing?      Aliases auto-fallback to original commands (safe)
1601    Preview init?        lean-ctx init --global --dry-run
1602
1603WEBSITE: https://leanctx.com
1604GITHUB:  https://github.com/yvgude/lean-ctx
1605",
1606        version = env!("CARGO_PKG_VERSION"),
1607    );
1608}