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            "buddy" | "pet" => {
1222                super::cloud::cmd_buddy(&rest);
1223                return;
1224            }
1225            "hook" => {
1226                hook_handlers::mark_hook_environment();
1227                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1228                let action = rest.first().map_or("help", std::string::String::as_str);
1229                match action {
1230                    "rewrite" => hook_handlers::handle_rewrite(),
1231                    "redirect" => hook_handlers::handle_redirect(),
1232                    "copilot" => hook_handlers::handle_copilot(),
1233                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1234                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1235                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1236                    _ => {
1237                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1238                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1239                        std::process::exit(1);
1240                    }
1241                }
1242                return;
1243            }
1244            "report-issue" | "report" => {
1245                report::run(&rest);
1246                return;
1247            }
1248            "uninstall" => {
1249                let dry_run = rest.iter().any(|a| a == "--dry-run");
1250                uninstall::run(dry_run);
1251                return;
1252            }
1253            "bypass" => {
1254                if rest.is_empty() {
1255                    eprintln!("Usage: lean-ctx bypass \"command\"");
1256                    eprintln!("Runs the command with zero compression (raw passthrough).");
1257                    std::process::exit(1);
1258                }
1259                let command = if rest.len() == 1 {
1260                    rest[0].clone()
1261                } else {
1262                    shell::join_command(&args[2..])
1263                };
1264                std::env::set_var("LEAN_CTX_RAW", "1");
1265                let code = shell::exec(&command);
1266                std::process::exit(code);
1267            }
1268            "safety-levels" | "safety" => {
1269                println!("{}", core::compression_safety::format_safety_table());
1270                return;
1271            }
1272            "cheat" | "cheatsheet" | "cheat-sheet" => {
1273                super::cmd_cheatsheet();
1274                return;
1275            }
1276            "login" => {
1277                super::cloud::cmd_login(&rest);
1278                return;
1279            }
1280            "register" => {
1281                super::cloud::cmd_register(&rest);
1282                return;
1283            }
1284            "forgot-password" => {
1285                super::cloud::cmd_forgot_password(&rest);
1286                return;
1287            }
1288            "sync" => {
1289                super::cloud::cmd_sync();
1290                return;
1291            }
1292            "contribute" => {
1293                super::cloud::cmd_contribute();
1294                return;
1295            }
1296            "cloud" => {
1297                super::cloud::cmd_cloud(&rest);
1298                return;
1299            }
1300            "upgrade" => {
1301                super::cloud::cmd_upgrade();
1302                return;
1303            }
1304            "--version" | "-V" => {
1305                println!("{}", core::integrity::origin_line());
1306                return;
1307            }
1308            "--help" | "-h" => {
1309                print_help();
1310                return;
1311            }
1312            "mcp" => {}
1313            _ => {
1314                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1315                print_help();
1316                std::process::exit(1);
1317            }
1318        }
1319    }
1320
1321    if let Err(e) = run_mcp_server() {
1322        tracing::error!("lean-ctx: {e}");
1323        std::process::exit(1);
1324    }
1325}
1326
1327fn passthrough(command: &str) -> ! {
1328    let (shell, flag) = shell::shell_and_flag();
1329    let status = std::process::Command::new(&shell)
1330        .arg(&flag)
1331        .arg(command)
1332        .env("LEAN_CTX_ACTIVE", "1")
1333        .status()
1334        .map_or(127, |s| s.code().unwrap_or(1));
1335    std::process::exit(status);
1336}
1337
1338fn run_async<F: std::future::Future>(future: F) -> F::Output {
1339    tokio::runtime::Runtime::new()
1340        .expect("failed to create async runtime")
1341        .block_on(future)
1342}
1343
1344fn run_mcp_server() -> Result<()> {
1345    use rmcp::ServiceExt;
1346
1347    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1348
1349    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1350
1351    // Concurrency hardening:
1352    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1353    // - Limits Tokio worker/blocking threads to avoid host degradation.
1354    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1355        "mcp-startup",
1356        std::time::Duration::from_secs(3),
1357        std::time::Duration::from_secs(30),
1358    );
1359
1360    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1361    let worker_threads = parallelism.clamp(1, 4);
1362    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1363
1364    let rt = tokio::runtime::Builder::new_multi_thread()
1365        .worker_threads(worker_threads)
1366        .max_blocking_threads(max_blocking_threads)
1367        .enable_all()
1368        .build()?;
1369
1370    let server = tools::create_server();
1371    drop(startup_lock);
1372
1373    rt.block_on(async {
1374        core::logging::init_mcp_logging();
1375
1376        tracing::info!(
1377            "lean-ctx v{} MCP server starting",
1378            env!("CARGO_PKG_VERSION")
1379        );
1380
1381        let transport =
1382            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1383        let service = match server.serve(transport).await {
1384            Ok(s) => s,
1385            Err(e) => {
1386                let msg = e.to_string();
1387                if msg.contains("expect initialized")
1388                    || msg.contains("context canceled")
1389                    || msg.contains("broken pipe")
1390                {
1391                    tracing::debug!("Client disconnected before init: {msg}");
1392                    return Ok(());
1393                }
1394                return Err(e.into());
1395            }
1396        };
1397        match service.waiting().await {
1398            Ok(reason) => {
1399                tracing::info!("MCP server stopped: {reason:?}");
1400            }
1401            Err(e) => {
1402                let msg = e.to_string();
1403                if msg.contains("broken pipe")
1404                    || msg.contains("connection reset")
1405                    || msg.contains("context canceled")
1406                {
1407                    tracing::info!("MCP server: transport closed ({msg})");
1408                } else {
1409                    tracing::error!("MCP server error: {msg}");
1410                }
1411            }
1412        }
1413
1414        core::stats::flush();
1415        core::heatmap::flush();
1416        core::mode_predictor::ModePredictor::flush();
1417        core::feedback::FeedbackStore::flush();
1418
1419        Ok(())
1420    })
1421}
1422
1423fn print_help() {
1424    println!(
1425        "lean-ctx {version} — Context Runtime for AI Agents
1426
142795+ compression patterns | 58 MCP tools | Context Continuity Protocol
1428
1429USAGE:
1430    lean-ctx                       Start MCP server (stdio)
1431    lean-ctx serve                 Start MCP server (Streamable HTTP)
1432    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1433    lean-ctx serve --stop          Stop running daemon
1434    lean-ctx serve --status        Show daemon status
1435    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1436    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1437    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1438    lean-ctx exec \"command\"        Same as -c
1439    lean-ctx shell                 Interactive shell with compression
1440
1441COMMANDS:
1442    gain                           Visual dashboard (colors, bars, sparklines, USD)
1443    gain --live                    Live mode: auto-refreshes every 1s in-place
1444    gain --graph                   30-day savings chart
1445    gain --daily                   Bordered day-by-day table with USD
1446    gain --json                    Raw JSON export of all stats
1447         token-report [--json]          Token + memory report (project + session + CEP)
1448    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1449    index <status|build|build-full|watch>  Codebase index utilities
1450    cep                            CEP impact report (score trends, cache, modes)
1451    watch                          Live TUI dashboard (real-time event stream)
1452    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1453    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1454    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1455    proxy status                   Show proxy statistics
1456    cache [list|clear|stats]       Show/manage file read cache
1457    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1458    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1459    benchmark run [path] [--json]  Run real benchmark on project files
1460    benchmark report [path]        Generate shareable Markdown report
1461    cheatsheet                     Command cheat sheet & workflow quick reference
1462    setup                          One-command setup: shell + editor + verify
1463    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1464    bootstrap                      Non-interactive setup + fix (zero-config)
1465    status [--json]                Show setup + MCP + rules status
1466    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1467    init --agent <name>            Configure MCP for specific editor/agent
1468    read <file> [-m mode]          Read file with compression
1469    diff <file1> <file2>           Compressed file diff
1470    grep <pattern> [path]          Search with compressed output
1471    find <pattern> [path]          Find files with compressed output
1472    ls [path]                      Directory listing with compression
1473    deps [path]                    Show project dependencies
1474    discover                       Find uncompressed commands in shell history
1475    ghost [--json]                 Ghost Token report: find hidden token waste
1476    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1477    session                        Show adoption statistics
1478    session task <desc>            Set current task
1479    session finding <summary>      Record a finding
1480    session save                   Save current session
1481    session load [id]              Load session (latest if no ID)
1482    knowledge remember <value> --category <c> --key <k>   Store a fact
1483    knowledge recall [query] [--category <c>]             Retrieve facts
1484    knowledge search <query>       Cross-project knowledge search
1485    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1486    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1487    knowledge remove --category <c> --key <k>             Remove a fact
1488    knowledge status               Knowledge base summary
1489    overview [task]                Project overview (task-contextualized if given)
1490    compress [--signatures]        Context compression checkpoint
1491    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1492    profile [list|show|diff|create|set]  Manage context profiles
1493    theme [list|set|export|import] Customize terminal colors and themes
1494    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1495    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1496    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1497    update [--check]               Self-update lean-ctx binary from GitHub Releases
1498    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1499    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1500    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1501    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1502    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1503                                   Code smell detection (Property Graph, 8 rules)
1504    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1505    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1506    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1507    uninstall                      Remove shell hook, MCP configs, and data directory
1508
1509SHELL HOOK PATTERNS (95+):
1510    git       status, log, diff, add, commit, push, pull, fetch, clone,
1511              branch, checkout, switch, merge, stash, tag, reset, remote
1512    docker    build, ps, images, logs, compose, exec, network
1513    npm/pnpm  install, test, run, list, outdated, audit
1514    cargo     build, test, check, clippy
1515    gh        pr list/view/create, issue list/view, run list/view
1516    kubectl   get pods/services/deployments, logs, describe, apply
1517    python    pip install/list/outdated, ruff check/format, poetry, uv
1518    linters   eslint, biome, prettier, golangci-lint
1519    builds    tsc, next build, vite build
1520    ruby      rubocop, bundle install/update, rake test, rails test
1521    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1522    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1523    utils     curl, grep/rg, find, ls, wget, env
1524    data      JSON schema extraction, log deduplication
1525
1526READ MODES:
1527    auto                           Auto-select optimal mode (default)
1528    full                           Full content (cached re-reads = 13 tokens)
1529    map                            Dependency graph + API signatures
1530    signatures                     tree-sitter AST extraction (18 languages)
1531    task                           Task-relevant filtering (requires ctx_session task)
1532    reference                      One-line reference stub (cheap cache key)
1533    aggressive                     Syntax-stripped content
1534    entropy                        Shannon entropy filtered
1535    diff                           Changed lines only
1536    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1537
1538ENVIRONMENT:
1539    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1540    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1541    LEAN_CTX_RAW=1                 Same as --raw for current command
1542    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1543    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1544
1545OPTIONS:
1546    --version, -V                  Show version
1547    --help, -h                     Show this help
1548
1549EXAMPLES:
1550    lean-ctx -c \"git status\"       Compressed git output
1551    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1552    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1553    lean-ctx gain                  Visual terminal dashboard
1554    lean-ctx gain --live           Live auto-updating terminal dashboard
1555    lean-ctx gain --graph          30-day savings chart
1556    lean-ctx gain --daily          Day-by-day breakdown with USD
1557         lean-ctx token-report --json   Machine-readable token + memory report
1558    lean-ctx dashboard             Open web dashboard at localhost:3333
1559    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1560    lean-ctx gain --wrapped        Wrapped report card (recommended)
1561    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1562    lean-ctx sessions list         List all CCP sessions
1563    lean-ctx sessions show         Show latest session state
1564    lean-ctx discover              Find missed savings in shell history
1565    lean-ctx setup                 One-command setup (shell + editors + verify)
1566    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1567    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1568    lean-ctx bootstrap --json      Machine-readable bootstrap report
1569    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1570    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1571    lean-ctx-off                   Disable all shell aliases
1572    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1573    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1574    lean-ctx-mode off              Same as lean-ctx-off
1575    lean-ctx-status                Show whether compression is active
1576    lean-ctx init --agent pi       Install Pi Coding Agent extension
1577    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1578    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1579    lean-ctx doctor --fix --json   Repair + machine-readable report
1580    lean-ctx status --json         Machine-readable current status
1581    lean-ctx session task \"implement auth\"
1582    lean-ctx session finding \"auth.rs:42 — missing validation\"
1583    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1584    lean-ctx knowledge recall \"authentication\"
1585    lean-ctx knowledge search \"database migration\"
1586    lean-ctx overview \"refactor auth module\"
1587    lean-ctx compress --signatures
1588    lean-ctx read src/main.rs -m map
1589    lean-ctx grep \"pub fn\" src/
1590    lean-ctx deps .
1591
1592CLOUD:
1593    cloud status                   Show cloud connection status
1594    login <email>                  Log into existing LeanCTX Cloud account
1595    register <email>               Create a new LeanCTX Cloud account
1596    forgot-password <email>        Send password reset email
1597    sync                           Upload local stats to cloud dashboard
1598    contribute                     Share anonymized compression data
1599
1600TROUBLESHOOTING:
1601    Commands broken?     lean-ctx-off             (fixes current session)
1602    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1603    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1604    Binary missing?      Aliases auto-fallback to original commands (safe)
1605    Preview init?        lean-ctx init --global --dry-run
1606
1607WEBSITE: https://leanctx.com
1608GITHUB:  https://github.com/yvgude/lean-ctx
1609",
1610        version = env!("CARGO_PKG_VERSION"),
1611    );
1612}