Skip to main content

lean_ctx/cli/
dispatch.rs

1use crate::{
2    core, dashboard, doctor, heatmap, hook_handlers, mcp_stdio, report, setup, shell, status,
3    token_report, tools, tui, uninstall,
4};
5use anyhow::Result;
6
7pub fn run() {
8    let args: Vec<String> = std::env::args().collect();
9
10    if args.len() > 1 {
11        let rest = args[2..].to_vec();
12
13        match args[1].as_str() {
14            "-c" | "exec" => {
15                let raw = rest.first().is_some_and(|a| a == "--raw");
16                let cmd_args = if raw { &args[3..] } else { &args[2..] };
17                let command = if cmd_args.len() == 1 {
18                    cmd_args[0].clone()
19                } else {
20                    shell::join_command(cmd_args)
21                };
22                if std::env::var("LEAN_CTX_ACTIVE").is_ok()
23                    || std::env::var("LEAN_CTX_DISABLED").is_ok()
24                {
25                    passthrough(&command);
26                }
27                if raw {
28                    std::env::set_var("LEAN_CTX_RAW", "1");
29                } else {
30                    std::env::set_var("LEAN_CTX_COMPRESS", "1");
31                }
32                let code = shell::exec(&command);
33                core::stats::flush();
34                core::heatmap::flush();
35                std::process::exit(code);
36            }
37            "-t" | "--track" => {
38                let cmd_args = &args[2..];
39                let code = if cmd_args.len() > 1 {
40                    shell::exec_argv(cmd_args)
41                } else {
42                    let command = cmd_args[0].clone();
43                    if std::env::var("LEAN_CTX_ACTIVE").is_ok()
44                        || std::env::var("LEAN_CTX_DISABLED").is_ok()
45                    {
46                        passthrough(&command);
47                    }
48                    shell::exec(&command)
49                };
50                core::stats::flush();
51                core::heatmap::flush();
52                std::process::exit(code);
53            }
54            "shell" | "--shell" => {
55                shell::interactive();
56                return;
57            }
58            "gain" => {
59                if rest.iter().any(|a| a == "--reset") {
60                    core::stats::reset_all();
61                    println!("Stats reset. All token savings data cleared.");
62                    return;
63                }
64                if rest.iter().any(|a| a == "--live" || a == "--watch") {
65                    core::stats::gain_live();
66                    return;
67                }
68                let model = rest.iter().enumerate().find_map(|(i, a)| {
69                    if let Some(v) = a.strip_prefix("--model=") {
70                        return Some(v.to_string());
71                    }
72                    if a == "--model" {
73                        return rest.get(i + 1).cloned();
74                    }
75                    None
76                });
77                let period = rest
78                    .iter()
79                    .enumerate()
80                    .find_map(|(i, a)| {
81                        if let Some(v) = a.strip_prefix("--period=") {
82                            return Some(v.to_string());
83                        }
84                        if a == "--period" {
85                            return rest.get(i + 1).cloned();
86                        }
87                        None
88                    })
89                    .unwrap_or_else(|| "all".to_string());
90                let limit = rest
91                    .iter()
92                    .enumerate()
93                    .find_map(|(i, a)| {
94                        if let Some(v) = a.strip_prefix("--limit=") {
95                            return v.parse::<usize>().ok();
96                        }
97                        if a == "--limit" {
98                            return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
99                        }
100                        None
101                    })
102                    .unwrap_or(10);
103
104                if rest.iter().any(|a| a == "--graph") {
105                    println!("{}", core::stats::format_gain_graph());
106                } else if rest.iter().any(|a| a == "--daily") {
107                    println!("{}", core::stats::format_gain_daily());
108                } else if rest.iter().any(|a| a == "--json") {
109                    println!(
110                        "{}",
111                        tools::ctx_gain::handle(
112                            "json",
113                            Some(&period),
114                            model.as_deref(),
115                            Some(limit)
116                        )
117                    );
118                } else if rest.iter().any(|a| a == "--score") {
119                    println!(
120                        "{}",
121                        tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
122                    );
123                } else if rest.iter().any(|a| a == "--cost") {
124                    println!(
125                        "{}",
126                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
127                    );
128                } else if rest.iter().any(|a| a == "--tasks") {
129                    println!(
130                        "{}",
131                        tools::ctx_gain::handle("tasks", None, None, Some(limit))
132                    );
133                } else if rest.iter().any(|a| a == "--agents") {
134                    println!(
135                        "{}",
136                        tools::ctx_gain::handle("agents", None, None, Some(limit))
137                    );
138                } else if rest.iter().any(|a| a == "--heatmap") {
139                    println!(
140                        "{}",
141                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
142                    );
143                } else if rest.iter().any(|a| a == "--wrapped") {
144                    println!(
145                        "{}",
146                        tools::ctx_gain::handle(
147                            "wrapped",
148                            Some(&period),
149                            model.as_deref(),
150                            Some(limit)
151                        )
152                    );
153                } else if rest.iter().any(|a| a == "--pipeline") {
154                    let stats_path = dirs::home_dir()
155                        .unwrap_or_default()
156                        .join(".lean-ctx")
157                        .join("pipeline_stats.json");
158                    if let Ok(data) = std::fs::read_to_string(&stats_path) {
159                        if let Ok(stats) =
160                            serde_json::from_str::<core::pipeline::PipelineStats>(&data)
161                        {
162                            println!("{}", stats.format_summary());
163                        } else {
164                            println!("No pipeline stats available yet (corrupt data).");
165                        }
166                    } else {
167                        println!(
168                            "No pipeline stats available yet. Use MCP tools to generate data."
169                        );
170                    }
171                } else if rest.iter().any(|a| a == "--deep") {
172                    println!(
173                        "{}\n{}\n{}\n{}\n{}",
174                        tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
175                        tools::ctx_gain::handle("tasks", None, None, Some(limit)),
176                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
177                        tools::ctx_gain::handle("agents", None, None, Some(limit)),
178                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
179                    );
180                } else {
181                    println!("{}", core::stats::format_gain());
182                }
183                return;
184            }
185            "token-report" | "report-tokens" => {
186                let code = token_report::run_cli(&rest);
187                if code != 0 {
188                    std::process::exit(code);
189                }
190                return;
191            }
192            "pack" => {
193                crate::cli::cmd_pack(&rest);
194                return;
195            }
196            "proof" => {
197                crate::cli::cmd_proof(&rest);
198                return;
199            }
200            "verify" => {
201                crate::cli::cmd_verify(&rest);
202                return;
203            }
204            "instructions" => {
205                crate::cli::cmd_instructions(&rest);
206                return;
207            }
208            "index" => {
209                crate::cli::cmd_index(&rest);
210                return;
211            }
212            "cep" => {
213                println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
214                return;
215            }
216            "dashboard" => {
217                if rest.iter().any(|a| a == "--help" || a == "-h") {
218                    println!("Usage: lean-ctx dashboard [--port=N] [--host=H] [--project=PATH]");
219                    println!("Examples:");
220                    println!("  lean-ctx dashboard");
221                    println!("  lean-ctx dashboard --port=3333");
222                    println!("  lean-ctx dashboard --host=0.0.0.0");
223                    return;
224                }
225                let port = rest
226                    .iter()
227                    .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
228                    .and_then(|p| p.parse().ok());
229                let host = rest
230                    .iter()
231                    .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
232                    .map(String::from);
233                let project = rest
234                    .iter()
235                    .find_map(|p| p.strip_prefix("--project="))
236                    .map(String::from);
237                if let Some(ref p) = project {
238                    std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
239                }
240                run_async(dashboard::start(port, host));
241                return;
242            }
243            "team" => {
244                let sub = rest.first().map_or("help", std::string::String::as_str);
245                match sub {
246                    "serve" => {
247                        #[cfg(feature = "team-server")]
248                        {
249                            let cfg_path = rest
250                                .iter()
251                                .enumerate()
252                                .find_map(|(i, a)| {
253                                    if let Some(v) = a.strip_prefix("--config=") {
254                                        return Some(v.to_string());
255                                    }
256                                    if a == "--config" {
257                                        return rest.get(i + 1).cloned();
258                                    }
259                                    None
260                                })
261                                .unwrap_or_default();
262
263                            if cfg_path.trim().is_empty() {
264                                eprintln!("Usage: lean-ctx team serve --config <path>");
265                                std::process::exit(1);
266                            }
267
268                            let cfg = crate::http_server::team::TeamServerConfig::load(
269                                std::path::Path::new(&cfg_path),
270                            )
271                            .unwrap_or_else(|e| {
272                                eprintln!("Invalid team config: {e}");
273                                std::process::exit(1);
274                            });
275
276                            if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
277                                tracing::error!("Team server error: {e}");
278                                std::process::exit(1);
279                            }
280                            return;
281                        }
282                        #[cfg(not(feature = "team-server"))]
283                        {
284                            eprintln!("lean-ctx team serve is not available in this build");
285                            std::process::exit(1);
286                        }
287                    }
288                    "token" => {
289                        let action = rest.get(1).map_or("help", std::string::String::as_str);
290                        if action == "create" {
291                            #[cfg(feature = "team-server")]
292                            {
293                                let args = &rest[2..];
294                                let cfg_path = args
295                                    .iter()
296                                    .enumerate()
297                                    .find_map(|(i, a)| {
298                                        if let Some(v) = a.strip_prefix("--config=") {
299                                            return Some(v.to_string());
300                                        }
301                                        if a == "--config" {
302                                            return args.get(i + 1).cloned();
303                                        }
304                                        None
305                                    })
306                                    .unwrap_or_default();
307                                let token_id = args
308                                    .iter()
309                                    .enumerate()
310                                    .find_map(|(i, a)| {
311                                        if let Some(v) = a.strip_prefix("--id=") {
312                                            return Some(v.to_string());
313                                        }
314                                        if a == "--id" {
315                                            return args.get(i + 1).cloned();
316                                        }
317                                        None
318                                    })
319                                    .unwrap_or_default();
320                                let scopes_csv = args
321                                    .iter()
322                                    .enumerate()
323                                    .find_map(|(i, a)| {
324                                        if let Some(v) = a.strip_prefix("--scopes=") {
325                                            return Some(v.to_string());
326                                        }
327                                        if let Some(v) = a.strip_prefix("--scope=") {
328                                            return Some(v.to_string());
329                                        }
330                                        if a == "--scopes" || a == "--scope" {
331                                            return args.get(i + 1).cloned();
332                                        }
333                                        None
334                                    })
335                                    .unwrap_or_default();
336
337                                if cfg_path.trim().is_empty()
338                                    || token_id.trim().is_empty()
339                                    || scopes_csv.trim().is_empty()
340                                {
341                                    eprintln!(
342                                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
343                                        );
344                                    std::process::exit(1);
345                                }
346
347                                let cfg_p = std::path::PathBuf::from(&cfg_path);
348                                let mut cfg = crate::http_server::team::TeamServerConfig::load(
349                                    cfg_p.as_path(),
350                                )
351                                .unwrap_or_else(|e| {
352                                    eprintln!("Invalid team config: {e}");
353                                    std::process::exit(1);
354                                });
355
356                                let mut scopes = Vec::new();
357                                for part in scopes_csv.split(',') {
358                                    let p = part.trim().to_ascii_lowercase();
359                                    if p.is_empty() {
360                                        continue;
361                                    }
362                                    let scope = match p.as_str() {
363                                        "search" => crate::http_server::team::TeamScope::Search,
364                                        "graph" => crate::http_server::team::TeamScope::Graph,
365                                        "artifacts" => {
366                                            crate::http_server::team::TeamScope::Artifacts
367                                        }
368                                        "index" => crate::http_server::team::TeamScope::Index,
369                                        "events" => crate::http_server::team::TeamScope::Events,
370                                        "sessionmutations" | "session_mutations" => {
371                                            crate::http_server::team::TeamScope::SessionMutations
372                                        }
373                                        "knowledge" => {
374                                            crate::http_server::team::TeamScope::Knowledge
375                                        }
376                                        "audit" => crate::http_server::team::TeamScope::Audit,
377                                        _ => {
378                                            eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
379                                            std::process::exit(1);
380                                        }
381                                    };
382                                    if !scopes.contains(&scope) {
383                                        scopes.push(scope);
384                                    }
385                                }
386                                if scopes.is_empty() {
387                                    eprintln!("At least 1 scope is required");
388                                    std::process::exit(1);
389                                }
390
391                                let (token, hash) = crate::http_server::team::create_token()
392                                    .unwrap_or_else(|e| {
393                                        eprintln!("Token generation failed: {e}");
394                                        std::process::exit(1);
395                                    });
396
397                                cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
398                                    id: token_id,
399                                    sha256_hex: hash,
400                                    scopes,
401                                });
402
403                                cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
404                                    eprintln!("Failed to write config: {e}");
405                                    std::process::exit(1);
406                                });
407
408                                println!("{token}");
409                                return;
410                            }
411
412                            #[cfg(not(feature = "team-server"))]
413                            {
414                                eprintln!("lean-ctx team token is not available in this build");
415                                std::process::exit(1);
416                            }
417                        }
418                        eprintln!(
419                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
420                        );
421                        std::process::exit(1);
422                    }
423                    "sync" => {
424                        #[cfg(feature = "team-server")]
425                        {
426                            let args = &rest[1..];
427                            let cfg_path = args
428                                .iter()
429                                .enumerate()
430                                .find_map(|(i, a)| {
431                                    if let Some(v) = a.strip_prefix("--config=") {
432                                        return Some(v.to_string());
433                                    }
434                                    if a == "--config" {
435                                        return args.get(i + 1).cloned();
436                                    }
437                                    None
438                                })
439                                .unwrap_or_default();
440                            if cfg_path.trim().is_empty() {
441                                eprintln!(
442                                    "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
443                                );
444                                std::process::exit(1);
445                            }
446                            let only_ws = args.iter().enumerate().find_map(|(i, a)| {
447                                if let Some(v) = a.strip_prefix("--workspace=") {
448                                    return Some(v.to_string());
449                                }
450                                if let Some(v) = a.strip_prefix("--workspace-id=") {
451                                    return Some(v.to_string());
452                                }
453                                if a == "--workspace" || a == "--workspace-id" {
454                                    return args.get(i + 1).cloned();
455                                }
456                                None
457                            });
458
459                            let cfg = crate::http_server::team::TeamServerConfig::load(
460                                std::path::Path::new(&cfg_path),
461                            )
462                            .unwrap_or_else(|e| {
463                                eprintln!("Invalid team config: {e}");
464                                std::process::exit(1);
465                            });
466
467                            for ws in &cfg.workspaces {
468                                if let Some(ref only) = only_ws {
469                                    if ws.id != *only {
470                                        continue;
471                                    }
472                                }
473                                let git_dir = ws.root.join(".git");
474                                if !git_dir.exists() {
475                                    eprintln!(
476                                        "workspace '{}' root is not a git repo: {}",
477                                        ws.id,
478                                        ws.root.display()
479                                    );
480                                    std::process::exit(1);
481                                }
482                                let status = std::process::Command::new("git")
483                                    .arg("-C")
484                                    .arg(&ws.root)
485                                    .args(["fetch", "--all", "--prune"])
486                                    .status()
487                                    .unwrap_or_else(|e| {
488                                        eprintln!(
489                                            "git fetch failed for workspace '{}': {e}",
490                                            ws.id
491                                        );
492                                        std::process::exit(1);
493                                    });
494                                if !status.success() {
495                                    eprintln!(
496                                        "git fetch failed for workspace '{}' (exit={})",
497                                        ws.id,
498                                        status.code().unwrap_or(1)
499                                    );
500                                    std::process::exit(1);
501                                }
502                            }
503                            return;
504                        }
505                        #[cfg(not(feature = "team-server"))]
506                        {
507                            eprintln!("lean-ctx team sync is not available in this build");
508                            std::process::exit(1);
509                        }
510                    }
511                    _ => {
512                        eprintln!(
513                            "Usage:\n  lean-ctx team serve --config <path>\n  lean-ctx team token create --config <path> --id <id> --scopes <csv>\n  lean-ctx team sync --config <path> [--workspace <id>]"
514                        );
515                        std::process::exit(1);
516                    }
517                }
518            }
519            "serve" => {
520                #[cfg(feature = "http-server")]
521                {
522                    let mut cfg = crate::http_server::HttpServerConfig::default();
523                    let mut daemon_mode = false;
524                    let mut stop_mode = false;
525                    let mut status_mode = false;
526                    let mut foreground_daemon = false;
527                    let mut i = 0;
528                    while i < rest.len() {
529                        match rest[i].as_str() {
530                            "--daemon" | "-d" => daemon_mode = true,
531                            "--stop" => stop_mode = true,
532                            "--status" => status_mode = true,
533                            "--_foreground-daemon" => foreground_daemon = true,
534                            "--host" | "-H" => {
535                                i += 1;
536                                if i < rest.len() {
537                                    cfg.host.clone_from(&rest[i]);
538                                }
539                            }
540                            arg if arg.starts_with("--host=") => {
541                                cfg.host = arg["--host=".len()..].to_string();
542                            }
543                            "--port" | "-p" => {
544                                i += 1;
545                                if i < rest.len() {
546                                    if let Ok(p) = rest[i].parse::<u16>() {
547                                        cfg.port = p;
548                                    }
549                                }
550                            }
551                            arg if arg.starts_with("--port=") => {
552                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
553                                    cfg.port = p;
554                                }
555                            }
556                            "--project-root" => {
557                                i += 1;
558                                if i < rest.len() {
559                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
560                                }
561                            }
562                            arg if arg.starts_with("--project-root=") => {
563                                cfg.project_root =
564                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
565                            }
566                            "--auth-token" => {
567                                i += 1;
568                                if i < rest.len() {
569                                    cfg.auth_token = Some(rest[i].clone());
570                                }
571                            }
572                            arg if arg.starts_with("--auth-token=") => {
573                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
574                            }
575                            "--stateful" => cfg.stateful_mode = true,
576                            "--stateless" => cfg.stateful_mode = false,
577                            "--json" => cfg.json_response = true,
578                            "--sse" => cfg.json_response = false,
579                            "--disable-host-check" => cfg.disable_host_check = true,
580                            "--allowed-host" => {
581                                i += 1;
582                                if i < rest.len() {
583                                    cfg.allowed_hosts.push(rest[i].clone());
584                                }
585                            }
586                            arg if arg.starts_with("--allowed-host=") => {
587                                cfg.allowed_hosts
588                                    .push(arg["--allowed-host=".len()..].to_string());
589                            }
590                            "--max-body-bytes" => {
591                                i += 1;
592                                if i < rest.len() {
593                                    if let Ok(n) = rest[i].parse::<usize>() {
594                                        cfg.max_body_bytes = n;
595                                    }
596                                }
597                            }
598                            arg if arg.starts_with("--max-body-bytes=") => {
599                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
600                                    cfg.max_body_bytes = n;
601                                }
602                            }
603                            "--max-concurrency" => {
604                                i += 1;
605                                if i < rest.len() {
606                                    if let Ok(n) = rest[i].parse::<usize>() {
607                                        cfg.max_concurrency = n;
608                                    }
609                                }
610                            }
611                            arg if arg.starts_with("--max-concurrency=") => {
612                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
613                                    cfg.max_concurrency = n;
614                                }
615                            }
616                            "--max-rps" => {
617                                i += 1;
618                                if i < rest.len() {
619                                    if let Ok(n) = rest[i].parse::<u32>() {
620                                        cfg.max_rps = n;
621                                    }
622                                }
623                            }
624                            arg if arg.starts_with("--max-rps=") => {
625                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
626                                    cfg.max_rps = n;
627                                }
628                            }
629                            "--rate-burst" => {
630                                i += 1;
631                                if i < rest.len() {
632                                    if let Ok(n) = rest[i].parse::<u32>() {
633                                        cfg.rate_burst = n;
634                                    }
635                                }
636                            }
637                            arg if arg.starts_with("--rate-burst=") => {
638                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
639                                    cfg.rate_burst = n;
640                                }
641                            }
642                            "--request-timeout-ms" => {
643                                i += 1;
644                                if i < rest.len() {
645                                    if let Ok(n) = rest[i].parse::<u64>() {
646                                        cfg.request_timeout_ms = n;
647                                    }
648                                }
649                            }
650                            arg if arg.starts_with("--request-timeout-ms=") => {
651                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
652                                    cfg.request_timeout_ms = n;
653                                }
654                            }
655                            "--help" | "-h" => {
656                                eprintln!(
657                                    "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
658                                     \\n\\
659                                     Options:\\n\\
660                                       --daemon, -d          Start as background daemon (UDS)\\n\\
661                                       --stop                Stop running daemon\\n\\
662                                       --status              Show daemon status\\n\\
663                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
664                                       --port, -p            Bind port (default: 8080)\\n\\
665                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
666                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
667                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
668                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
669                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
670                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
671                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
672                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
673                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
674                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
675                                       --disable-host-check  Disable Host header validation (unsafe)"
676                                );
677                                return;
678                            }
679                            _ => {}
680                        }
681                        i += 1;
682                    }
683
684                    #[cfg(unix)]
685                    {
686                        if stop_mode {
687                            if let Err(e) = crate::daemon::stop_daemon() {
688                                eprintln!("Error: {e}");
689                                std::process::exit(1);
690                            }
691                            return;
692                        }
693
694                        if status_mode {
695                            println!("{}", crate::daemon::daemon_status());
696                            return;
697                        }
698
699                        if daemon_mode {
700                            if let Err(e) = crate::daemon::start_daemon(&rest) {
701                                eprintln!("Error: {e}");
702                                std::process::exit(1);
703                            }
704                            return;
705                        }
706
707                        if foreground_daemon {
708                            if let Err(e) = crate::daemon::init_foreground_daemon() {
709                                eprintln!("Error writing PID file: {e}");
710                                std::process::exit(1);
711                            }
712                            let socket_path = crate::daemon::daemon_socket_path();
713                            if let Err(e) =
714                                run_async(crate::http_server::serve_uds(cfg.clone(), socket_path))
715                            {
716                                tracing::error!("Daemon server error: {e}");
717                                crate::daemon::cleanup_daemon_files();
718                                std::process::exit(1);
719                            }
720                            crate::daemon::cleanup_daemon_files();
721                            return;
722                        }
723                    }
724
725                    #[cfg(not(unix))]
726                    {
727                        if stop_mode || status_mode || daemon_mode || foreground_daemon {
728                            eprintln!("Daemon mode is only supported on Unix systems.");
729                            std::process::exit(1);
730                        }
731                    }
732
733                    if cfg.auth_token.is_none() {
734                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
735                            if !v.trim().is_empty() {
736                                cfg.auth_token = Some(v);
737                            }
738                        }
739                    }
740
741                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
742                        tracing::error!("HTTP server error: {e}");
743                        std::process::exit(1);
744                    }
745                    return;
746                }
747                #[cfg(not(feature = "http-server"))]
748                {
749                    eprintln!("lean-ctx serve is not available in this build");
750                    std::process::exit(1);
751                }
752            }
753            "watch" => {
754                if rest.iter().any(|a| a == "--help" || a == "-h") {
755                    println!("Usage: lean-ctx watch");
756                    println!("  Live TUI dashboard (real-time event stream).");
757                    return;
758                }
759                if let Err(e) = tui::run() {
760                    tracing::error!("TUI error: {e}");
761                    std::process::exit(1);
762                }
763                return;
764            }
765            "proxy" => {
766                #[cfg(feature = "http-server")]
767                {
768                    let sub = rest.first().map_or("help", std::string::String::as_str);
769                    match sub {
770                        "start" => {
771                            let port: u16 = rest
772                                .iter()
773                                .find_map(|p| {
774                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
775                                })
776                                .and_then(|p| p.parse().ok())
777                                .unwrap_or(4444);
778                            let autostart = rest.iter().any(|a| a == "--autostart");
779                            if autostart {
780                                crate::proxy_autostart::install(port, false);
781                                return;
782                            }
783                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
784                                tracing::error!("Proxy error: {e}");
785                                std::process::exit(1);
786                            }
787                        }
788                        "stop" => {
789                            match ureq::get(&format!(
790                                "http://127.0.0.1:{}/health",
791                                rest.iter()
792                                    .find_map(|p| p.strip_prefix("--port="))
793                                    .and_then(|p| p.parse::<u16>().ok())
794                                    .unwrap_or(4444)
795                            ))
796                            .call()
797                            {
798                                Ok(_) => {
799                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
800                                }
801                                Err(_) => {
802                                    println!("No proxy running on that port.");
803                                }
804                            }
805                        }
806                        "status" => {
807                            let port: u16 = rest
808                                .iter()
809                                .find_map(|p| p.strip_prefix("--port="))
810                                .and_then(|p| p.parse().ok())
811                                .unwrap_or(4444);
812                            if let Ok(resp) =
813                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
814                            {
815                                let body = resp.into_body().read_to_string().unwrap_or_default();
816                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
817                                    println!("lean-ctx proxy status:");
818                                    println!("  Requests:    {}", v["requests_total"]);
819                                    println!("  Compressed:  {}", v["requests_compressed"]);
820                                    println!("  Tokens saved: {}", v["tokens_saved"]);
821                                    println!(
822                                        "  Compression: {}%",
823                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
824                                    );
825                                } else {
826                                    println!("{body}");
827                                }
828                            } else {
829                                println!("No proxy running on port {port}.");
830                                println!("Start with: lean-ctx proxy start");
831                            }
832                        }
833                        _ => {
834                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
835                        }
836                    }
837                    return;
838                }
839                #[cfg(not(feature = "http-server"))]
840                {
841                    eprintln!("lean-ctx proxy is not available in this build");
842                    std::process::exit(1);
843                }
844            }
845            "init" => {
846                super::cmd_init(&rest);
847                return;
848            }
849            "setup" => {
850                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
851                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
852                let fix = rest.iter().any(|a| a == "--fix");
853                let json = rest.iter().any(|a| a == "--json");
854
855                if non_interactive || fix || json || yes {
856                    let opts = setup::SetupOptions {
857                        non_interactive,
858                        yes,
859                        fix,
860                        json,
861                    };
862                    match setup::run_setup_with_options(opts) {
863                        Ok(report) => {
864                            if json {
865                                println!(
866                                    "{}",
867                                    serde_json::to_string_pretty(&report)
868                                        .unwrap_or_else(|_| "{}".to_string())
869                                );
870                            }
871                            if !report.success {
872                                std::process::exit(1);
873                            }
874                        }
875                        Err(e) => {
876                            eprintln!("{e}");
877                            std::process::exit(1);
878                        }
879                    }
880                } else {
881                    setup::run_setup();
882                }
883                return;
884            }
885            "install" => {
886                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
887                let json = rest.iter().any(|a| a == "--json");
888                if !repair {
889                    eprintln!("Usage: lean-ctx install --repair [--json]");
890                    std::process::exit(1);
891                }
892                let opts = setup::SetupOptions {
893                    non_interactive: true,
894                    yes: true,
895                    fix: true,
896                    json,
897                };
898                match setup::run_setup_with_options(opts) {
899                    Ok(report) => {
900                        if json {
901                            println!(
902                                "{}",
903                                serde_json::to_string_pretty(&report)
904                                    .unwrap_or_else(|_| "{}".to_string())
905                            );
906                        }
907                        if !report.success {
908                            std::process::exit(1);
909                        }
910                    }
911                    Err(e) => {
912                        eprintln!("{e}");
913                        std::process::exit(1);
914                    }
915                }
916                return;
917            }
918            "bootstrap" => {
919                let json = rest.iter().any(|a| a == "--json");
920                let opts = setup::SetupOptions {
921                    non_interactive: true,
922                    yes: true,
923                    fix: true,
924                    json,
925                };
926                match setup::run_setup_with_options(opts) {
927                    Ok(report) => {
928                        if json {
929                            println!(
930                                "{}",
931                                serde_json::to_string_pretty(&report)
932                                    .unwrap_or_else(|_| "{}".to_string())
933                            );
934                        }
935                        if !report.success {
936                            std::process::exit(1);
937                        }
938                    }
939                    Err(e) => {
940                        eprintln!("{e}");
941                        std::process::exit(1);
942                    }
943                }
944                return;
945            }
946            "status" => {
947                let code = status::run_cli(&rest);
948                if code != 0 {
949                    std::process::exit(code);
950                }
951                return;
952            }
953            "read" => {
954                super::cmd_read(&rest);
955                core::stats::flush();
956                return;
957            }
958            "diff" => {
959                super::cmd_diff(&rest);
960                core::stats::flush();
961                return;
962            }
963            "grep" => {
964                super::cmd_grep(&rest);
965                core::stats::flush();
966                return;
967            }
968            "find" => {
969                super::cmd_find(&rest);
970                core::stats::flush();
971                return;
972            }
973            "ls" => {
974                super::cmd_ls(&rest);
975                core::stats::flush();
976                return;
977            }
978            "deps" => {
979                super::cmd_deps(&rest);
980                core::stats::flush();
981                return;
982            }
983            "discover" => {
984                super::cmd_discover(&rest);
985                return;
986            }
987            "ghost" => {
988                super::cmd_ghost(&rest);
989                return;
990            }
991            "filter" => {
992                super::cmd_filter(&rest);
993                return;
994            }
995            "heatmap" => {
996                heatmap::cmd_heatmap(&rest);
997                return;
998            }
999            "graph" => {
1000                let sub = rest.first().map_or("build", std::string::String::as_str);
1001                match sub {
1002                    "build" => {
1003                        let root = rest.get(1).cloned().or_else(|| {
1004                            std::env::current_dir()
1005                                .ok()
1006                                .map(|p| p.to_string_lossy().to_string())
1007                        });
1008                        let root = root.unwrap_or_else(|| ".".to_string());
1009                        let index = core::graph_index::load_or_build(&root);
1010                        println!(
1011                            "Graph built: {} files, {} edges",
1012                            index.files.len(),
1013                            index.edges.len()
1014                        );
1015                    }
1016                    "export-html" => {
1017                        let mut root: Option<String> = None;
1018                        let mut out: Option<String> = None;
1019                        let mut max_nodes: usize = 2500;
1020
1021                        let args = &rest[1..];
1022                        let mut i = 0usize;
1023                        while i < args.len() {
1024                            let a = args[i].as_str();
1025                            if let Some(v) = a.strip_prefix("--root=") {
1026                                root = Some(v.to_string());
1027                            } else if a == "--root" {
1028                                root = args.get(i + 1).cloned();
1029                                i += 1;
1030                            } else if let Some(v) = a.strip_prefix("--out=") {
1031                                out = Some(v.to_string());
1032                            } else if a == "--out" {
1033                                out = args.get(i + 1).cloned();
1034                                i += 1;
1035                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1036                                max_nodes = v.parse::<usize>().unwrap_or(0);
1037                            } else if a == "--max-nodes" {
1038                                let v = args.get(i + 1).map_or("", String::as_str);
1039                                max_nodes = v.parse::<usize>().unwrap_or(0);
1040                                i += 1;
1041                            }
1042                            i += 1;
1043                        }
1044
1045                        let root = root
1046                            .or_else(|| {
1047                                std::env::current_dir()
1048                                    .ok()
1049                                    .map(|p| p.to_string_lossy().to_string())
1050                            })
1051                            .unwrap_or_else(|| ".".to_string());
1052                        let Some(out) = out else {
1053                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1054                            std::process::exit(1);
1055                        };
1056                        if max_nodes == 0 {
1057                            eprintln!("--max-nodes must be >= 1");
1058                            std::process::exit(1);
1059                        }
1060
1061                        core::graph_export::export_graph_html(
1062                            &root,
1063                            std::path::Path::new(&out),
1064                            max_nodes,
1065                        )
1066                        .unwrap_or_else(|e| {
1067                            eprintln!("graph export failed: {e}");
1068                            std::process::exit(1);
1069                        });
1070                        println!("{out}");
1071                    }
1072                    _ => {
1073                        eprintln!(
1074                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1075                        );
1076                        std::process::exit(1);
1077                    }
1078                }
1079                return;
1080            }
1081            "smells" => {
1082                let action = rest.first().map_or("summary", String::as_str);
1083                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1084                    if let Some(v) = a.strip_prefix("--rule=") {
1085                        return Some(v.to_string());
1086                    }
1087                    if a == "--rule" {
1088                        return rest.get(i + 1).cloned();
1089                    }
1090                    None
1091                });
1092                let path = rest.iter().enumerate().find_map(|(i, a)| {
1093                    if let Some(v) = a.strip_prefix("--path=") {
1094                        return Some(v.to_string());
1095                    }
1096                    if a == "--path" {
1097                        return rest.get(i + 1).cloned();
1098                    }
1099                    None
1100                });
1101                let root = rest
1102                    .iter()
1103                    .enumerate()
1104                    .find_map(|(i, a)| {
1105                        if let Some(v) = a.strip_prefix("--root=") {
1106                            return Some(v.to_string());
1107                        }
1108                        if a == "--root" {
1109                            return rest.get(i + 1).cloned();
1110                        }
1111                        None
1112                    })
1113                    .or_else(|| {
1114                        std::env::current_dir()
1115                            .ok()
1116                            .map(|p| p.to_string_lossy().to_string())
1117                    })
1118                    .unwrap_or_else(|| ".".to_string());
1119                let fmt = if rest.iter().any(|a| a == "--json") {
1120                    Some("json")
1121                } else {
1122                    None
1123                };
1124                println!(
1125                    "{}",
1126                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1127                );
1128                return;
1129            }
1130            "session" => {
1131                super::cmd_session_action(&rest);
1132                return;
1133            }
1134            "control" | "context-control" => {
1135                super::cmd_control(&rest);
1136                return;
1137            }
1138            "plan" | "context-plan" => {
1139                super::cmd_plan(&rest);
1140                return;
1141            }
1142            "compile" | "context-compile" => {
1143                super::cmd_compile(&rest);
1144                return;
1145            }
1146            "knowledge" => {
1147                super::cmd_knowledge(&rest);
1148                return;
1149            }
1150            "overview" => {
1151                super::cmd_overview(&rest);
1152                return;
1153            }
1154            "compress" => {
1155                super::cmd_compress(&rest);
1156                return;
1157            }
1158            "wrapped" => {
1159                super::cmd_wrapped(&rest);
1160                return;
1161            }
1162            "sessions" => {
1163                super::cmd_sessions(&rest);
1164                return;
1165            }
1166            "benchmark" => {
1167                super::cmd_benchmark(&rest);
1168                return;
1169            }
1170            "profile" => {
1171                super::cmd_profile(&rest);
1172                return;
1173            }
1174            "config" => {
1175                super::cmd_config(&rest);
1176                return;
1177            }
1178            "stats" => {
1179                super::cmd_stats(&rest);
1180                return;
1181            }
1182            "cache" => {
1183                super::cmd_cache(&rest);
1184                return;
1185            }
1186            "theme" => {
1187                super::cmd_theme(&rest);
1188                return;
1189            }
1190            "tee" => {
1191                super::cmd_tee(&rest);
1192                return;
1193            }
1194            "terse" | "compression" => {
1195                super::cmd_compression(&rest);
1196                return;
1197            }
1198            "slow-log" => {
1199                super::cmd_slow_log(&rest);
1200                return;
1201            }
1202            "update" | "--self-update" => {
1203                core::updater::run(&rest);
1204                return;
1205            }
1206            "doctor" => {
1207                let code = doctor::run_cli(&rest);
1208                if code != 0 {
1209                    std::process::exit(code);
1210                }
1211                return;
1212            }
1213            "gotchas" | "bugs" => {
1214                super::cloud::cmd_gotchas(&rest);
1215                return;
1216            }
1217            "buddy" | "pet" => {
1218                super::cloud::cmd_buddy(&rest);
1219                return;
1220            }
1221            "hook" => {
1222                hook_handlers::mark_hook_environment();
1223                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1224                let action = rest.first().map_or("help", std::string::String::as_str);
1225                match action {
1226                    "rewrite" => hook_handlers::handle_rewrite(),
1227                    "redirect" => hook_handlers::handle_redirect(),
1228                    "copilot" => hook_handlers::handle_copilot(),
1229                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1230                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1231                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1232                    _ => {
1233                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1234                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1235                        std::process::exit(1);
1236                    }
1237                }
1238                return;
1239            }
1240            "report-issue" | "report" => {
1241                report::run(&rest);
1242                return;
1243            }
1244            "uninstall" => {
1245                let dry_run = rest.iter().any(|a| a == "--dry-run");
1246                uninstall::run(dry_run);
1247                return;
1248            }
1249            "bypass" => {
1250                if rest.is_empty() {
1251                    eprintln!("Usage: lean-ctx bypass \"command\"");
1252                    eprintln!("Runs the command with zero compression (raw passthrough).");
1253                    std::process::exit(1);
1254                }
1255                let command = if rest.len() == 1 {
1256                    rest[0].clone()
1257                } else {
1258                    shell::join_command(&args[2..])
1259                };
1260                std::env::set_var("LEAN_CTX_RAW", "1");
1261                let code = shell::exec(&command);
1262                std::process::exit(code);
1263            }
1264            "safety-levels" | "safety" => {
1265                println!("{}", core::compression_safety::format_safety_table());
1266                return;
1267            }
1268            "cheat" | "cheatsheet" | "cheat-sheet" => {
1269                super::cmd_cheatsheet();
1270                return;
1271            }
1272            "login" => {
1273                super::cloud::cmd_login(&rest);
1274                return;
1275            }
1276            "register" => {
1277                super::cloud::cmd_register(&rest);
1278                return;
1279            }
1280            "forgot-password" => {
1281                super::cloud::cmd_forgot_password(&rest);
1282                return;
1283            }
1284            "sync" => {
1285                super::cloud::cmd_sync();
1286                return;
1287            }
1288            "contribute" => {
1289                super::cloud::cmd_contribute();
1290                return;
1291            }
1292            "cloud" => {
1293                super::cloud::cmd_cloud(&rest);
1294                return;
1295            }
1296            "upgrade" => {
1297                super::cloud::cmd_upgrade();
1298                return;
1299            }
1300            "--version" | "-V" => {
1301                println!("{}", core::integrity::origin_line());
1302                return;
1303            }
1304            "--help" | "-h" => {
1305                print_help();
1306                return;
1307            }
1308            "mcp" => {}
1309            _ => {
1310                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1311                print_help();
1312                std::process::exit(1);
1313            }
1314        }
1315    }
1316
1317    if let Err(e) = run_mcp_server() {
1318        tracing::error!("lean-ctx: {e}");
1319        std::process::exit(1);
1320    }
1321}
1322
1323fn passthrough(command: &str) -> ! {
1324    let (shell, flag) = shell::shell_and_flag();
1325    let status = std::process::Command::new(&shell)
1326        .arg(&flag)
1327        .arg(command)
1328        .env("LEAN_CTX_ACTIVE", "1")
1329        .status()
1330        .map_or(127, |s| s.code().unwrap_or(1));
1331    std::process::exit(status);
1332}
1333
1334fn run_async<F: std::future::Future>(future: F) -> F::Output {
1335    tokio::runtime::Runtime::new()
1336        .expect("failed to create async runtime")
1337        .block_on(future)
1338}
1339
1340fn run_mcp_server() -> Result<()> {
1341    use rmcp::ServiceExt;
1342
1343    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1344
1345    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1346
1347    // Concurrency hardening:
1348    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1349    // - Limits Tokio worker/blocking threads to avoid host degradation.
1350    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1351        "mcp-startup",
1352        std::time::Duration::from_secs(3),
1353        std::time::Duration::from_secs(30),
1354    );
1355
1356    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1357    let worker_threads = parallelism.clamp(1, 4);
1358    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1359
1360    let rt = tokio::runtime::Builder::new_multi_thread()
1361        .worker_threads(worker_threads)
1362        .max_blocking_threads(max_blocking_threads)
1363        .enable_all()
1364        .build()?;
1365
1366    let server = tools::create_server();
1367    drop(startup_lock);
1368
1369    rt.block_on(async {
1370        core::logging::init_mcp_logging();
1371
1372        tracing::info!(
1373            "lean-ctx v{} MCP server starting",
1374            env!("CARGO_PKG_VERSION")
1375        );
1376
1377        let transport =
1378            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1379        let service = match server.serve(transport).await {
1380            Ok(s) => s,
1381            Err(e) => {
1382                let msg = e.to_string();
1383                if msg.contains("expect initialized")
1384                    || msg.contains("context canceled")
1385                    || msg.contains("broken pipe")
1386                {
1387                    tracing::debug!("Client disconnected before init: {msg}");
1388                    return Ok(());
1389                }
1390                return Err(e.into());
1391            }
1392        };
1393        match service.waiting().await {
1394            Ok(reason) => {
1395                tracing::info!("MCP server stopped: {reason:?}");
1396            }
1397            Err(e) => {
1398                let msg = e.to_string();
1399                if msg.contains("broken pipe")
1400                    || msg.contains("connection reset")
1401                    || msg.contains("context canceled")
1402                {
1403                    tracing::info!("MCP server: transport closed ({msg})");
1404                } else {
1405                    tracing::error!("MCP server error: {msg}");
1406                }
1407            }
1408        }
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 | 58 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}