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