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    let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
10    if !enters_mcp {
11        crate::core::logging::init_logging();
12    }
13
14    if args.len() > 1 {
15        let rest = args[2..].to_vec();
16
17        match args[1].as_str() {
18            "-c" | "exec" => {
19                let raw = rest.first().is_some_and(|a| a == "--raw");
20                let cmd_args = if raw { &args[3..] } else { &args[2..] };
21                let command = if cmd_args.len() == 1 {
22                    cmd_args[0].clone()
23                } else {
24                    shell::join_command(cmd_args)
25                };
26                if std::env::var("LEAN_CTX_ACTIVE").is_ok()
27                    || std::env::var("LEAN_CTX_DISABLED").is_ok()
28                {
29                    passthrough(&command);
30                }
31                if raw {
32                    std::env::set_var("LEAN_CTX_RAW", "1");
33                } else {
34                    std::env::set_var("LEAN_CTX_COMPRESS", "1");
35                }
36                let code = shell::exec(&command);
37                core::stats::flush();
38                core::heatmap::flush();
39                std::process::exit(code);
40            }
41            "-t" | "--track" => {
42                let cmd_args = &args[2..];
43                let code = if cmd_args.len() > 1 {
44                    shell::exec_argv(cmd_args)
45                } else {
46                    let command = cmd_args[0].clone();
47                    if std::env::var("LEAN_CTX_ACTIVE").is_ok()
48                        || std::env::var("LEAN_CTX_DISABLED").is_ok()
49                    {
50                        passthrough(&command);
51                    }
52                    shell::exec(&command)
53                };
54                core::stats::flush();
55                core::heatmap::flush();
56                std::process::exit(code);
57            }
58            "shell" | "--shell" => {
59                shell::interactive();
60                return;
61            }
62            "gain" => {
63                if rest.iter().any(|a| a == "--reset") {
64                    core::stats::reset_all();
65                    println!("Stats reset. All token savings data cleared.");
66                    return;
67                }
68                if rest.iter().any(|a| a == "--live" || a == "--watch") {
69                    core::stats::gain_live();
70                    return;
71                }
72                let model = rest.iter().enumerate().find_map(|(i, a)| {
73                    if let Some(v) = a.strip_prefix("--model=") {
74                        return Some(v.to_string());
75                    }
76                    if a == "--model" {
77                        return rest.get(i + 1).cloned();
78                    }
79                    None
80                });
81                let period = rest
82                    .iter()
83                    .enumerate()
84                    .find_map(|(i, a)| {
85                        if let Some(v) = a.strip_prefix("--period=") {
86                            return Some(v.to_string());
87                        }
88                        if a == "--period" {
89                            return rest.get(i + 1).cloned();
90                        }
91                        None
92                    })
93                    .unwrap_or_else(|| "all".to_string());
94                let limit = rest
95                    .iter()
96                    .enumerate()
97                    .find_map(|(i, a)| {
98                        if let Some(v) = a.strip_prefix("--limit=") {
99                            return v.parse::<usize>().ok();
100                        }
101                        if a == "--limit" {
102                            return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
103                        }
104                        None
105                    })
106                    .unwrap_or(10);
107
108                if rest.iter().any(|a| a == "--graph") {
109                    println!("{}", core::stats::format_gain_graph());
110                } else if rest.iter().any(|a| a == "--daily") {
111                    println!("{}", core::stats::format_gain_daily());
112                } else if rest.iter().any(|a| a == "--json") {
113                    println!(
114                        "{}",
115                        tools::ctx_gain::handle(
116                            "json",
117                            Some(&period),
118                            model.as_deref(),
119                            Some(limit)
120                        )
121                    );
122                } else if rest.iter().any(|a| a == "--score") {
123                    println!(
124                        "{}",
125                        tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
126                    );
127                } else if rest.iter().any(|a| a == "--cost") {
128                    println!(
129                        "{}",
130                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
131                    );
132                } else if rest.iter().any(|a| a == "--tasks") {
133                    println!(
134                        "{}",
135                        tools::ctx_gain::handle("tasks", None, None, Some(limit))
136                    );
137                } else if rest.iter().any(|a| a == "--agents") {
138                    println!(
139                        "{}",
140                        tools::ctx_gain::handle("agents", None, None, Some(limit))
141                    );
142                } else if rest.iter().any(|a| a == "--heatmap") {
143                    println!(
144                        "{}",
145                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
146                    );
147                } else if rest.iter().any(|a| a == "--wrapped") {
148                    println!(
149                        "{}",
150                        tools::ctx_gain::handle(
151                            "wrapped",
152                            Some(&period),
153                            model.as_deref(),
154                            Some(limit)
155                        )
156                    );
157                } else if rest.iter().any(|a| a == "--pipeline") {
158                    let stats_path = dirs::home_dir()
159                        .unwrap_or_default()
160                        .join(".lean-ctx")
161                        .join("pipeline_stats.json");
162                    if let Ok(data) = std::fs::read_to_string(&stats_path) {
163                        if let Ok(stats) =
164                            serde_json::from_str::<core::pipeline::PipelineStats>(&data)
165                        {
166                            println!("{}", stats.format_summary());
167                        } else {
168                            println!("No pipeline stats available yet (corrupt data).");
169                        }
170                    } else {
171                        println!(
172                            "No pipeline stats available yet. Use MCP tools to generate data."
173                        );
174                    }
175                } else if rest.iter().any(|a| a == "--deep") {
176                    println!(
177                        "{}\n{}\n{}\n{}\n{}",
178                        tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
179                        tools::ctx_gain::handle("tasks", None, None, Some(limit)),
180                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
181                        tools::ctx_gain::handle("agents", None, None, Some(limit)),
182                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
183                    );
184                } else {
185                    println!("{}", core::stats::format_gain());
186                }
187                return;
188            }
189            "token-report" | "report-tokens" => {
190                let code = token_report::run_cli(&rest);
191                if code != 0 {
192                    std::process::exit(code);
193                }
194                return;
195            }
196            "pack" => {
197                crate::cli::cmd_pack(&rest);
198                return;
199            }
200            "proof" => {
201                crate::cli::cmd_proof(&rest);
202                return;
203            }
204            "verify" => {
205                crate::cli::cmd_verify(&rest);
206                return;
207            }
208            "instructions" => {
209                crate::cli::cmd_instructions(&rest);
210                return;
211            }
212            "index" => {
213                crate::cli::cmd_index(&rest);
214                return;
215            }
216            "cep" => {
217                println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
218                return;
219            }
220            "dashboard" => {
221                if rest.iter().any(|a| a == "--help" || a == "-h") {
222                    println!("Usage: lean-ctx dashboard [--port=N] [--host=H] [--project=PATH]");
223                    println!("Examples:");
224                    println!("  lean-ctx dashboard");
225                    println!("  lean-ctx dashboard --port=3333");
226                    println!("  lean-ctx dashboard --host=0.0.0.0");
227                    return;
228                }
229                let port = rest
230                    .iter()
231                    .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
232                    .and_then(|p| p.parse().ok());
233                let host = rest
234                    .iter()
235                    .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
236                    .map(String::from);
237                let project = rest
238                    .iter()
239                    .find_map(|p| p.strip_prefix("--project="))
240                    .map(String::from);
241                if let Some(ref p) = project {
242                    std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
243                }
244                run_async(dashboard::start(port, host));
245                return;
246            }
247            "team" => {
248                let sub = rest.first().map_or("help", std::string::String::as_str);
249                match sub {
250                    "serve" => {
251                        #[cfg(feature = "team-server")]
252                        {
253                            let cfg_path = rest
254                                .iter()
255                                .enumerate()
256                                .find_map(|(i, a)| {
257                                    if let Some(v) = a.strip_prefix("--config=") {
258                                        return Some(v.to_string());
259                                    }
260                                    if a == "--config" {
261                                        return rest.get(i + 1).cloned();
262                                    }
263                                    None
264                                })
265                                .unwrap_or_default();
266
267                            if cfg_path.trim().is_empty() {
268                                eprintln!("Usage: lean-ctx team serve --config <path>");
269                                std::process::exit(1);
270                            }
271
272                            let cfg = crate::http_server::team::TeamServerConfig::load(
273                                std::path::Path::new(&cfg_path),
274                            )
275                            .unwrap_or_else(|e| {
276                                eprintln!("Invalid team config: {e}");
277                                std::process::exit(1);
278                            });
279
280                            if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
281                                tracing::error!("Team server error: {e}");
282                                std::process::exit(1);
283                            }
284                            return;
285                        }
286                        #[cfg(not(feature = "team-server"))]
287                        {
288                            eprintln!("lean-ctx team serve is not available in this build");
289                            std::process::exit(1);
290                        }
291                    }
292                    "token" => {
293                        let action = rest.get(1).map_or("help", std::string::String::as_str);
294                        if action == "create" {
295                            #[cfg(feature = "team-server")]
296                            {
297                                let args = &rest[2..];
298                                let cfg_path = args
299                                    .iter()
300                                    .enumerate()
301                                    .find_map(|(i, a)| {
302                                        if let Some(v) = a.strip_prefix("--config=") {
303                                            return Some(v.to_string());
304                                        }
305                                        if a == "--config" {
306                                            return args.get(i + 1).cloned();
307                                        }
308                                        None
309                                    })
310                                    .unwrap_or_default();
311                                let token_id = args
312                                    .iter()
313                                    .enumerate()
314                                    .find_map(|(i, a)| {
315                                        if let Some(v) = a.strip_prefix("--id=") {
316                                            return Some(v.to_string());
317                                        }
318                                        if a == "--id" {
319                                            return args.get(i + 1).cloned();
320                                        }
321                                        None
322                                    })
323                                    .unwrap_or_default();
324                                let scopes_csv = args
325                                    .iter()
326                                    .enumerate()
327                                    .find_map(|(i, a)| {
328                                        if let Some(v) = a.strip_prefix("--scopes=") {
329                                            return Some(v.to_string());
330                                        }
331                                        if let Some(v) = a.strip_prefix("--scope=") {
332                                            return Some(v.to_string());
333                                        }
334                                        if a == "--scopes" || a == "--scope" {
335                                            return args.get(i + 1).cloned();
336                                        }
337                                        None
338                                    })
339                                    .unwrap_or_default();
340
341                                if cfg_path.trim().is_empty()
342                                    || token_id.trim().is_empty()
343                                    || scopes_csv.trim().is_empty()
344                                {
345                                    eprintln!(
346                                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
347                                        );
348                                    std::process::exit(1);
349                                }
350
351                                let cfg_p = std::path::PathBuf::from(&cfg_path);
352                                let mut cfg = crate::http_server::team::TeamServerConfig::load(
353                                    cfg_p.as_path(),
354                                )
355                                .unwrap_or_else(|e| {
356                                    eprintln!("Invalid team config: {e}");
357                                    std::process::exit(1);
358                                });
359
360                                let mut scopes = Vec::new();
361                                for part in scopes_csv.split(',') {
362                                    let p = part.trim().to_ascii_lowercase();
363                                    if p.is_empty() {
364                                        continue;
365                                    }
366                                    let scope = match p.as_str() {
367                                        "search" => crate::http_server::team::TeamScope::Search,
368                                        "graph" => crate::http_server::team::TeamScope::Graph,
369                                        "artifacts" => {
370                                            crate::http_server::team::TeamScope::Artifacts
371                                        }
372                                        "index" => crate::http_server::team::TeamScope::Index,
373                                        "events" => crate::http_server::team::TeamScope::Events,
374                                        "sessionmutations" | "session_mutations" => {
375                                            crate::http_server::team::TeamScope::SessionMutations
376                                        }
377                                        "knowledge" => {
378                                            crate::http_server::team::TeamScope::Knowledge
379                                        }
380                                        "audit" => crate::http_server::team::TeamScope::Audit,
381                                        _ => {
382                                            eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
383                                            std::process::exit(1);
384                                        }
385                                    };
386                                    if !scopes.contains(&scope) {
387                                        scopes.push(scope);
388                                    }
389                                }
390                                if scopes.is_empty() {
391                                    eprintln!("At least 1 scope is required");
392                                    std::process::exit(1);
393                                }
394
395                                let (token, hash) = crate::http_server::team::create_token()
396                                    .unwrap_or_else(|e| {
397                                        eprintln!("Token generation failed: {e}");
398                                        std::process::exit(1);
399                                    });
400
401                                cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
402                                    id: token_id,
403                                    sha256_hex: hash,
404                                    scopes,
405                                });
406
407                                cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
408                                    eprintln!("Failed to write config: {e}");
409                                    std::process::exit(1);
410                                });
411
412                                println!("{token}");
413                                return;
414                            }
415
416                            #[cfg(not(feature = "team-server"))]
417                            {
418                                eprintln!("lean-ctx team token is not available in this build");
419                                std::process::exit(1);
420                            }
421                        }
422                        eprintln!(
423                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
424                        );
425                        std::process::exit(1);
426                    }
427                    "sync" => {
428                        #[cfg(feature = "team-server")]
429                        {
430                            let args = &rest[1..];
431                            let cfg_path = args
432                                .iter()
433                                .enumerate()
434                                .find_map(|(i, a)| {
435                                    if let Some(v) = a.strip_prefix("--config=") {
436                                        return Some(v.to_string());
437                                    }
438                                    if a == "--config" {
439                                        return args.get(i + 1).cloned();
440                                    }
441                                    None
442                                })
443                                .unwrap_or_default();
444                            if cfg_path.trim().is_empty() {
445                                eprintln!(
446                                    "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
447                                );
448                                std::process::exit(1);
449                            }
450                            let only_ws = args.iter().enumerate().find_map(|(i, a)| {
451                                if let Some(v) = a.strip_prefix("--workspace=") {
452                                    return Some(v.to_string());
453                                }
454                                if let Some(v) = a.strip_prefix("--workspace-id=") {
455                                    return Some(v.to_string());
456                                }
457                                if a == "--workspace" || a == "--workspace-id" {
458                                    return args.get(i + 1).cloned();
459                                }
460                                None
461                            });
462
463                            let cfg = crate::http_server::team::TeamServerConfig::load(
464                                std::path::Path::new(&cfg_path),
465                            )
466                            .unwrap_or_else(|e| {
467                                eprintln!("Invalid team config: {e}");
468                                std::process::exit(1);
469                            });
470
471                            for ws in &cfg.workspaces {
472                                if let Some(ref only) = only_ws {
473                                    if ws.id != *only {
474                                        continue;
475                                    }
476                                }
477                                let git_dir = ws.root.join(".git");
478                                if !git_dir.exists() {
479                                    eprintln!(
480                                        "workspace '{}' root is not a git repo: {}",
481                                        ws.id,
482                                        ws.root.display()
483                                    );
484                                    std::process::exit(1);
485                                }
486                                let status = std::process::Command::new("git")
487                                    .arg("-C")
488                                    .arg(&ws.root)
489                                    .args(["fetch", "--all", "--prune"])
490                                    .status()
491                                    .unwrap_or_else(|e| {
492                                        eprintln!(
493                                            "git fetch failed for workspace '{}': {e}",
494                                            ws.id
495                                        );
496                                        std::process::exit(1);
497                                    });
498                                if !status.success() {
499                                    eprintln!(
500                                        "git fetch failed for workspace '{}' (exit={})",
501                                        ws.id,
502                                        status.code().unwrap_or(1)
503                                    );
504                                    std::process::exit(1);
505                                }
506                            }
507                            return;
508                        }
509                        #[cfg(not(feature = "team-server"))]
510                        {
511                            eprintln!("lean-ctx team sync is not available in this build");
512                            std::process::exit(1);
513                        }
514                    }
515                    _ => {
516                        eprintln!(
517                            "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>]"
518                        );
519                        std::process::exit(1);
520                    }
521                }
522            }
523            "serve" => {
524                #[cfg(feature = "http-server")]
525                {
526                    let mut cfg = crate::http_server::HttpServerConfig::default();
527                    let mut daemon_mode = false;
528                    let mut stop_mode = false;
529                    let mut status_mode = false;
530                    let mut foreground_daemon = false;
531                    let mut i = 0;
532                    while i < rest.len() {
533                        match rest[i].as_str() {
534                            "--daemon" | "-d" => daemon_mode = true,
535                            "--stop" => stop_mode = true,
536                            "--status" => status_mode = true,
537                            "--_foreground-daemon" => foreground_daemon = true,
538                            "--host" | "-H" => {
539                                i += 1;
540                                if i < rest.len() {
541                                    cfg.host.clone_from(&rest[i]);
542                                }
543                            }
544                            arg if arg.starts_with("--host=") => {
545                                cfg.host = arg["--host=".len()..].to_string();
546                            }
547                            "--port" | "-p" => {
548                                i += 1;
549                                if i < rest.len() {
550                                    if let Ok(p) = rest[i].parse::<u16>() {
551                                        cfg.port = p;
552                                    }
553                                }
554                            }
555                            arg if arg.starts_with("--port=") => {
556                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
557                                    cfg.port = p;
558                                }
559                            }
560                            "--project-root" => {
561                                i += 1;
562                                if i < rest.len() {
563                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
564                                }
565                            }
566                            arg if arg.starts_with("--project-root=") => {
567                                cfg.project_root =
568                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
569                            }
570                            "--auth-token" => {
571                                i += 1;
572                                if i < rest.len() {
573                                    cfg.auth_token = Some(rest[i].clone());
574                                }
575                            }
576                            arg if arg.starts_with("--auth-token=") => {
577                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
578                            }
579                            "--stateful" => cfg.stateful_mode = true,
580                            "--stateless" => cfg.stateful_mode = false,
581                            "--json" => cfg.json_response = true,
582                            "--sse" => cfg.json_response = false,
583                            "--disable-host-check" => cfg.disable_host_check = true,
584                            "--allowed-host" => {
585                                i += 1;
586                                if i < rest.len() {
587                                    cfg.allowed_hosts.push(rest[i].clone());
588                                }
589                            }
590                            arg if arg.starts_with("--allowed-host=") => {
591                                cfg.allowed_hosts
592                                    .push(arg["--allowed-host=".len()..].to_string());
593                            }
594                            "--max-body-bytes" => {
595                                i += 1;
596                                if i < rest.len() {
597                                    if let Ok(n) = rest[i].parse::<usize>() {
598                                        cfg.max_body_bytes = n;
599                                    }
600                                }
601                            }
602                            arg if arg.starts_with("--max-body-bytes=") => {
603                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
604                                    cfg.max_body_bytes = n;
605                                }
606                            }
607                            "--max-concurrency" => {
608                                i += 1;
609                                if i < rest.len() {
610                                    if let Ok(n) = rest[i].parse::<usize>() {
611                                        cfg.max_concurrency = n;
612                                    }
613                                }
614                            }
615                            arg if arg.starts_with("--max-concurrency=") => {
616                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
617                                    cfg.max_concurrency = n;
618                                }
619                            }
620                            "--max-rps" => {
621                                i += 1;
622                                if i < rest.len() {
623                                    if let Ok(n) = rest[i].parse::<u32>() {
624                                        cfg.max_rps = n;
625                                    }
626                                }
627                            }
628                            arg if arg.starts_with("--max-rps=") => {
629                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
630                                    cfg.max_rps = n;
631                                }
632                            }
633                            "--rate-burst" => {
634                                i += 1;
635                                if i < rest.len() {
636                                    if let Ok(n) = rest[i].parse::<u32>() {
637                                        cfg.rate_burst = n;
638                                    }
639                                }
640                            }
641                            arg if arg.starts_with("--rate-burst=") => {
642                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
643                                    cfg.rate_burst = n;
644                                }
645                            }
646                            "--request-timeout-ms" => {
647                                i += 1;
648                                if i < rest.len() {
649                                    if let Ok(n) = rest[i].parse::<u64>() {
650                                        cfg.request_timeout_ms = n;
651                                    }
652                                }
653                            }
654                            arg if arg.starts_with("--request-timeout-ms=") => {
655                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
656                                    cfg.request_timeout_ms = n;
657                                }
658                            }
659                            "--help" | "-h" => {
660                                eprintln!(
661                                    "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
662                                     \\n\\
663                                     Options:\\n\\
664                                       --daemon, -d          Start as background daemon (UDS)\\n\\
665                                       --stop                Stop running daemon\\n\\
666                                       --status              Show daemon status\\n\\
667                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
668                                       --port, -p            Bind port (default: 8080)\\n\\
669                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
670                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
671                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
672                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
673                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
674                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
675                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
676                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
677                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
678                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
679                                       --disable-host-check  Disable Host header validation (unsafe)"
680                                );
681                                return;
682                            }
683                            _ => {}
684                        }
685                        i += 1;
686                    }
687
688                    if stop_mode {
689                        if let Err(e) = crate::daemon::stop_daemon() {
690                            eprintln!("Error: {e}");
691                            std::process::exit(1);
692                        }
693                        return;
694                    }
695
696                    if status_mode {
697                        println!("{}", crate::daemon::daemon_status());
698                        return;
699                    }
700
701                    if daemon_mode {
702                        if let Err(e) = crate::daemon::start_daemon(&rest) {
703                            eprintln!("Error: {e}");
704                            std::process::exit(1);
705                        }
706                        return;
707                    }
708
709                    if foreground_daemon {
710                        if let Err(e) = crate::daemon::init_foreground_daemon() {
711                            eprintln!("Error writing PID file: {e}");
712                            std::process::exit(1);
713                        }
714                        let addr = crate::daemon::daemon_addr();
715                        if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
716                        {
717                            tracing::error!("Daemon server error: {e}");
718                            crate::daemon::cleanup_daemon_files();
719                            std::process::exit(1);
720                        }
721                        crate::daemon::cleanup_daemon_files();
722                        return;
723                    }
724
725                    if cfg.auth_token.is_none() {
726                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
727                            if !v.trim().is_empty() {
728                                cfg.auth_token = Some(v);
729                            }
730                        }
731                    }
732
733                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
734                        tracing::error!("HTTP server error: {e}");
735                        std::process::exit(1);
736                    }
737                    return;
738                }
739                #[cfg(not(feature = "http-server"))]
740                {
741                    eprintln!("lean-ctx serve is not available in this build");
742                    std::process::exit(1);
743                }
744            }
745            "watch" => {
746                if rest.iter().any(|a| a == "--help" || a == "-h") {
747                    println!("Usage: lean-ctx watch");
748                    println!("  Live TUI dashboard (real-time event stream).");
749                    return;
750                }
751                if let Err(e) = tui::run() {
752                    tracing::error!("TUI error: {e}");
753                    std::process::exit(1);
754                }
755                return;
756            }
757            "proxy" => {
758                #[cfg(feature = "http-server")]
759                {
760                    let sub = rest.first().map_or("help", std::string::String::as_str);
761                    match sub {
762                        "start" => {
763                            let port: u16 = rest
764                                .iter()
765                                .find_map(|p| {
766                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
767                                })
768                                .and_then(|p| p.parse().ok())
769                                .unwrap_or(4444);
770                            let autostart = rest.iter().any(|a| a == "--autostart");
771                            if autostart {
772                                crate::proxy_autostart::install(port, false);
773                                return;
774                            }
775                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
776                                tracing::error!("Proxy error: {e}");
777                                std::process::exit(1);
778                            }
779                        }
780                        "stop" => {
781                            match ureq::get(&format!(
782                                "http://127.0.0.1:{}/health",
783                                rest.iter()
784                                    .find_map(|p| p.strip_prefix("--port="))
785                                    .and_then(|p| p.parse::<u16>().ok())
786                                    .unwrap_or(4444)
787                            ))
788                            .call()
789                            {
790                                Ok(_) => {
791                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
792                                }
793                                Err(_) => {
794                                    println!("No proxy running on that port.");
795                                }
796                            }
797                        }
798                        "status" => {
799                            let port: u16 = rest
800                                .iter()
801                                .find_map(|p| p.strip_prefix("--port="))
802                                .and_then(|p| p.parse().ok())
803                                .unwrap_or(4444);
804                            if let Ok(resp) =
805                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
806                            {
807                                let body = resp.into_body().read_to_string().unwrap_or_default();
808                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
809                                    println!("lean-ctx proxy status:");
810                                    println!("  Requests:    {}", v["requests_total"]);
811                                    println!("  Compressed:  {}", v["requests_compressed"]);
812                                    println!("  Tokens saved: {}", v["tokens_saved"]);
813                                    println!(
814                                        "  Compression: {}%",
815                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
816                                    );
817                                } else {
818                                    println!("{body}");
819                                }
820                            } else {
821                                println!("No proxy running on port {port}.");
822                                println!("Start with: lean-ctx proxy start");
823                            }
824                        }
825                        _ => {
826                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
827                        }
828                    }
829                    return;
830                }
831                #[cfg(not(feature = "http-server"))]
832                {
833                    eprintln!("lean-ctx proxy is not available in this build");
834                    std::process::exit(1);
835                }
836            }
837            "init" => {
838                super::cmd_init(&rest);
839                return;
840            }
841            "setup" => {
842                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
843                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
844                let fix = rest.iter().any(|a| a == "--fix");
845                let json = rest.iter().any(|a| a == "--json");
846                let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
847
848                if non_interactive || fix || json || yes {
849                    let opts = setup::SetupOptions {
850                        non_interactive,
851                        yes,
852                        fix,
853                        json,
854                        no_auto_approve,
855                    };
856                    match setup::run_setup_with_options(opts) {
857                        Ok(report) => {
858                            if json {
859                                println!(
860                                    "{}",
861                                    serde_json::to_string_pretty(&report)
862                                        .unwrap_or_else(|_| "{}".to_string())
863                                );
864                            }
865                            if !report.success {
866                                std::process::exit(1);
867                            }
868                        }
869                        Err(e) => {
870                            eprintln!("{e}");
871                            std::process::exit(1);
872                        }
873                    }
874                } else {
875                    setup::run_setup();
876                }
877                return;
878            }
879            "install" => {
880                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
881                let json = rest.iter().any(|a| a == "--json");
882                if !repair {
883                    eprintln!("Usage: lean-ctx install --repair [--json]");
884                    std::process::exit(1);
885                }
886                let opts = setup::SetupOptions {
887                    non_interactive: true,
888                    yes: true,
889                    fix: true,
890                    json,
891                    ..Default::default()
892                };
893                match setup::run_setup_with_options(opts) {
894                    Ok(report) => {
895                        if json {
896                            println!(
897                                "{}",
898                                serde_json::to_string_pretty(&report)
899                                    .unwrap_or_else(|_| "{}".to_string())
900                            );
901                        }
902                        if !report.success {
903                            std::process::exit(1);
904                        }
905                    }
906                    Err(e) => {
907                        eprintln!("{e}");
908                        std::process::exit(1);
909                    }
910                }
911                return;
912            }
913            "bootstrap" => {
914                let json = rest.iter().any(|a| a == "--json");
915                let opts = setup::SetupOptions {
916                    non_interactive: true,
917                    yes: true,
918                    fix: true,
919                    json,
920                    ..Default::default()
921                };
922                match setup::run_setup_with_options(opts) {
923                    Ok(report) => {
924                        if json {
925                            println!(
926                                "{}",
927                                serde_json::to_string_pretty(&report)
928                                    .unwrap_or_else(|_| "{}".to_string())
929                            );
930                        }
931                        if !report.success {
932                            std::process::exit(1);
933                        }
934                    }
935                    Err(e) => {
936                        eprintln!("{e}");
937                        std::process::exit(1);
938                    }
939                }
940                return;
941            }
942            "status" => {
943                let code = status::run_cli(&rest);
944                if code != 0 {
945                    std::process::exit(code);
946                }
947                return;
948            }
949            "read" => {
950                super::cmd_read(&rest);
951                core::stats::flush();
952                return;
953            }
954            "diff" => {
955                super::cmd_diff(&rest);
956                core::stats::flush();
957                return;
958            }
959            "grep" => {
960                super::cmd_grep(&rest);
961                core::stats::flush();
962                return;
963            }
964            "find" => {
965                super::cmd_find(&rest);
966                core::stats::flush();
967                return;
968            }
969            "ls" => {
970                super::cmd_ls(&rest);
971                core::stats::flush();
972                return;
973            }
974            "deps" => {
975                super::cmd_deps(&rest);
976                core::stats::flush();
977                return;
978            }
979            "discover" => {
980                super::cmd_discover(&rest);
981                return;
982            }
983            "ghost" => {
984                super::cmd_ghost(&rest);
985                return;
986            }
987            "filter" => {
988                super::cmd_filter(&rest);
989                return;
990            }
991            "heatmap" => {
992                heatmap::cmd_heatmap(&rest);
993                return;
994            }
995            "graph" => {
996                let sub = rest.first().map_or("build", std::string::String::as_str);
997                match sub {
998                    "build" => {
999                        let root = rest.get(1).cloned().or_else(|| {
1000                            std::env::current_dir()
1001                                .ok()
1002                                .map(|p| p.to_string_lossy().to_string())
1003                        });
1004                        let root = root.unwrap_or_else(|| ".".to_string());
1005                        let index = core::graph_index::load_or_build(&root);
1006                        println!(
1007                            "Graph built: {} files, {} edges",
1008                            index.files.len(),
1009                            index.edges.len()
1010                        );
1011                    }
1012                    "export-html" => {
1013                        let mut root: Option<String> = None;
1014                        let mut out: Option<String> = None;
1015                        let mut max_nodes: usize = 2500;
1016
1017                        let args = &rest[1..];
1018                        let mut i = 0usize;
1019                        while i < args.len() {
1020                            let a = args[i].as_str();
1021                            if let Some(v) = a.strip_prefix("--root=") {
1022                                root = Some(v.to_string());
1023                            } else if a == "--root" {
1024                                root = args.get(i + 1).cloned();
1025                                i += 1;
1026                            } else if let Some(v) = a.strip_prefix("--out=") {
1027                                out = Some(v.to_string());
1028                            } else if a == "--out" {
1029                                out = args.get(i + 1).cloned();
1030                                i += 1;
1031                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1032                                max_nodes = v.parse::<usize>().unwrap_or(0);
1033                            } else if a == "--max-nodes" {
1034                                let v = args.get(i + 1).map_or("", String::as_str);
1035                                max_nodes = v.parse::<usize>().unwrap_or(0);
1036                                i += 1;
1037                            }
1038                            i += 1;
1039                        }
1040
1041                        let root = root
1042                            .or_else(|| {
1043                                std::env::current_dir()
1044                                    .ok()
1045                                    .map(|p| p.to_string_lossy().to_string())
1046                            })
1047                            .unwrap_or_else(|| ".".to_string());
1048                        let Some(out) = out else {
1049                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1050                            std::process::exit(1);
1051                        };
1052                        if max_nodes == 0 {
1053                            eprintln!("--max-nodes must be >= 1");
1054                            std::process::exit(1);
1055                        }
1056
1057                        core::graph_export::export_graph_html(
1058                            &root,
1059                            std::path::Path::new(&out),
1060                            max_nodes,
1061                        )
1062                        .unwrap_or_else(|e| {
1063                            eprintln!("graph export failed: {e}");
1064                            std::process::exit(1);
1065                        });
1066                        println!("{out}");
1067                    }
1068                    _ => {
1069                        eprintln!(
1070                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1071                        );
1072                        std::process::exit(1);
1073                    }
1074                }
1075                return;
1076            }
1077            "smells" => {
1078                let action = rest.first().map_or("summary", String::as_str);
1079                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1080                    if let Some(v) = a.strip_prefix("--rule=") {
1081                        return Some(v.to_string());
1082                    }
1083                    if a == "--rule" {
1084                        return rest.get(i + 1).cloned();
1085                    }
1086                    None
1087                });
1088                let path = rest.iter().enumerate().find_map(|(i, a)| {
1089                    if let Some(v) = a.strip_prefix("--path=") {
1090                        return Some(v.to_string());
1091                    }
1092                    if a == "--path" {
1093                        return rest.get(i + 1).cloned();
1094                    }
1095                    None
1096                });
1097                let root = rest
1098                    .iter()
1099                    .enumerate()
1100                    .find_map(|(i, a)| {
1101                        if let Some(v) = a.strip_prefix("--root=") {
1102                            return Some(v.to_string());
1103                        }
1104                        if a == "--root" {
1105                            return rest.get(i + 1).cloned();
1106                        }
1107                        None
1108                    })
1109                    .or_else(|| {
1110                        std::env::current_dir()
1111                            .ok()
1112                            .map(|p| p.to_string_lossy().to_string())
1113                    })
1114                    .unwrap_or_else(|| ".".to_string());
1115                let fmt = if rest.iter().any(|a| a == "--json") {
1116                    Some("json")
1117                } else {
1118                    None
1119                };
1120                println!(
1121                    "{}",
1122                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1123                );
1124                return;
1125            }
1126            "session" => {
1127                super::cmd_session_action(&rest);
1128                return;
1129            }
1130            "control" | "context-control" => {
1131                super::cmd_control(&rest);
1132                return;
1133            }
1134            "plan" | "context-plan" => {
1135                super::cmd_plan(&rest);
1136                return;
1137            }
1138            "compile" | "context-compile" => {
1139                super::cmd_compile(&rest);
1140                return;
1141            }
1142            "knowledge" => {
1143                super::cmd_knowledge(&rest);
1144                return;
1145            }
1146            "overview" => {
1147                super::cmd_overview(&rest);
1148                return;
1149            }
1150            "compress" => {
1151                super::cmd_compress(&rest);
1152                return;
1153            }
1154            "wrapped" => {
1155                super::cmd_wrapped(&rest);
1156                return;
1157            }
1158            "sessions" => {
1159                super::cmd_sessions(&rest);
1160                return;
1161            }
1162            "benchmark" => {
1163                super::cmd_benchmark(&rest);
1164                return;
1165            }
1166            "profile" => {
1167                super::cmd_profile(&rest);
1168                return;
1169            }
1170            "config" => {
1171                super::cmd_config(&rest);
1172                return;
1173            }
1174            "stats" => {
1175                super::cmd_stats(&rest);
1176                return;
1177            }
1178            "cache" => {
1179                super::cmd_cache(&rest);
1180                return;
1181            }
1182            "theme" => {
1183                super::cmd_theme(&rest);
1184                return;
1185            }
1186            "tee" => {
1187                super::cmd_tee(&rest);
1188                return;
1189            }
1190            "terse" | "compression" => {
1191                super::cmd_compression(&rest);
1192                return;
1193            }
1194            "slow-log" => {
1195                super::cmd_slow_log(&rest);
1196                return;
1197            }
1198            "update" | "--self-update" => {
1199                core::updater::run(&rest);
1200                return;
1201            }
1202            "doctor" => {
1203                let code = doctor::run_cli(&rest);
1204                if code != 0 {
1205                    std::process::exit(code);
1206                }
1207                return;
1208            }
1209            "gotchas" | "bugs" => {
1210                super::cloud::cmd_gotchas(&rest);
1211                return;
1212            }
1213            "learn" => {
1214                super::cmd_learn(&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                    "observe" => hook_handlers::handle_observe(),
1229                    "copilot" => hook_handlers::handle_copilot(),
1230                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1231                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1232                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1233                    _ => {
1234                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1235                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1236                        std::process::exit(1);
1237                    }
1238                }
1239                return;
1240            }
1241            "report-issue" | "report" => {
1242                report::run(&rest);
1243                return;
1244            }
1245            "uninstall" => {
1246                let dry_run = rest.iter().any(|a| a == "--dry-run");
1247                uninstall::run(dry_run);
1248                return;
1249            }
1250            "bypass" => {
1251                if rest.is_empty() {
1252                    eprintln!("Usage: lean-ctx bypass \"command\"");
1253                    eprintln!("Runs the command with zero compression (raw passthrough).");
1254                    std::process::exit(1);
1255                }
1256                let command = if rest.len() == 1 {
1257                    rest[0].clone()
1258                } else {
1259                    shell::join_command(&args[2..])
1260                };
1261                std::env::set_var("LEAN_CTX_RAW", "1");
1262                let code = shell::exec(&command);
1263                std::process::exit(code);
1264            }
1265            "safety-levels" | "safety" => {
1266                println!("{}", core::compression_safety::format_safety_table());
1267                return;
1268            }
1269            "cheat" | "cheatsheet" | "cheat-sheet" => {
1270                super::cmd_cheatsheet();
1271                return;
1272            }
1273            "login" => {
1274                super::cloud::cmd_login(&rest);
1275                return;
1276            }
1277            "register" => {
1278                super::cloud::cmd_register(&rest);
1279                return;
1280            }
1281            "forgot-password" => {
1282                super::cloud::cmd_forgot_password(&rest);
1283                return;
1284            }
1285            "sync" => {
1286                super::cloud::cmd_sync();
1287                return;
1288            }
1289            "contribute" => {
1290                super::cloud::cmd_contribute();
1291                return;
1292            }
1293            "cloud" => {
1294                super::cloud::cmd_cloud(&rest);
1295                return;
1296            }
1297            "upgrade" => {
1298                super::cloud::cmd_upgrade();
1299                return;
1300            }
1301            "--version" | "-V" => {
1302                println!("{}", core::integrity::origin_line());
1303                return;
1304            }
1305            "--help" | "-h" => {
1306                print_help();
1307                return;
1308            }
1309            "mcp" => {}
1310            _ => {
1311                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1312                print_help();
1313                std::process::exit(1);
1314            }
1315        }
1316    }
1317
1318    if let Err(e) = run_mcp_server() {
1319        tracing::error!("lean-ctx: {e}");
1320        std::process::exit(1);
1321    }
1322}
1323
1324fn passthrough(command: &str) -> ! {
1325    let (shell, flag) = shell::shell_and_flag();
1326    let status = std::process::Command::new(&shell)
1327        .arg(&flag)
1328        .arg(command)
1329        .env("LEAN_CTX_ACTIVE", "1")
1330        .status()
1331        .map_or(127, |s| s.code().unwrap_or(1));
1332    std::process::exit(status);
1333}
1334
1335fn run_async<F: std::future::Future>(future: F) -> F::Output {
1336    tokio::runtime::Runtime::new()
1337        .expect("failed to create async runtime")
1338        .block_on(future)
1339}
1340
1341fn run_mcp_server() -> Result<()> {
1342    use rmcp::ServiceExt;
1343
1344    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1345
1346    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1347
1348    // Concurrency hardening:
1349    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1350    // - Limits Tokio worker/blocking threads to avoid host degradation.
1351    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1352        "mcp-startup",
1353        std::time::Duration::from_secs(3),
1354        std::time::Duration::from_secs(30),
1355    );
1356
1357    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1358    let worker_threads = parallelism.clamp(1, 4);
1359    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1360
1361    let rt = tokio::runtime::Builder::new_multi_thread()
1362        .worker_threads(worker_threads)
1363        .max_blocking_threads(max_blocking_threads)
1364        .enable_all()
1365        .build()?;
1366
1367    let server = tools::create_server();
1368    drop(startup_lock);
1369
1370    rt.block_on(async {
1371        core::logging::init_mcp_logging();
1372        core::protocol::set_mcp_context(true);
1373
1374        tracing::info!(
1375            "lean-ctx v{} MCP server starting",
1376            env!("CARGO_PKG_VERSION")
1377        );
1378
1379        let transport =
1380            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1381        let server_handle = server.clone();
1382        let service = match server.serve(transport).await {
1383            Ok(s) => s,
1384            Err(e) => {
1385                let msg = e.to_string();
1386                if msg.contains("expect initialized")
1387                    || msg.contains("context canceled")
1388                    || msg.contains("broken pipe")
1389                {
1390                    tracing::debug!("Client disconnected before init: {msg}");
1391                    return Ok(());
1392                }
1393                return Err(e.into());
1394            }
1395        };
1396        match service.waiting().await {
1397            Ok(reason) => {
1398                tracing::info!("MCP server stopped: {reason:?}");
1399            }
1400            Err(e) => {
1401                let msg = e.to_string();
1402                if msg.contains("broken pipe")
1403                    || msg.contains("connection reset")
1404                    || msg.contains("context canceled")
1405                {
1406                    tracing::info!("MCP server: transport closed ({msg})");
1407                } else {
1408                    tracing::error!("MCP server error: {msg}");
1409                }
1410            }
1411        }
1412
1413        server_handle.shutdown().await;
1414
1415        core::stats::flush();
1416        core::heatmap::flush();
1417        core::mode_predictor::ModePredictor::flush();
1418        core::feedback::FeedbackStore::flush();
1419
1420        Ok(())
1421    })
1422}
1423
1424fn print_help() {
1425    println!(
1426        "lean-ctx {version} — Context Runtime for AI Agents
1427
142895+ compression patterns | 59 MCP tools | Context Continuity Protocol
1429
1430USAGE:
1431    lean-ctx                       Start MCP server (stdio)
1432    lean-ctx serve                 Start MCP server (Streamable HTTP)
1433    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1434    lean-ctx serve --stop          Stop running daemon
1435    lean-ctx serve --status        Show daemon status
1436    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1437    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1438    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1439    lean-ctx exec \"command\"        Same as -c
1440    lean-ctx shell                 Interactive shell with compression
1441
1442COMMANDS:
1443    gain                           Visual dashboard (colors, bars, sparklines, USD)
1444    gain --live                    Live mode: auto-refreshes every 1s in-place
1445    gain --graph                   30-day savings chart
1446    gain --daily                   Bordered day-by-day table with USD
1447    gain --json                    Raw JSON export of all stats
1448         token-report [--json]          Token + memory report (project + session + CEP)
1449    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1450    index <status|build|build-full|watch>  Codebase index utilities
1451    cep                            CEP impact report (score trends, cache, modes)
1452    watch                          Live TUI dashboard (real-time event stream)
1453    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1454    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1455    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1456    proxy status                   Show proxy statistics
1457    cache [list|clear|stats]       Show/manage file read cache
1458    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1459    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1460    benchmark run [path] [--json]  Run real benchmark on project files
1461    benchmark report [path]        Generate shareable Markdown report
1462    cheatsheet                     Command cheat sheet & workflow quick reference
1463    setup                          One-command setup: shell + editor + verify
1464    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1465    bootstrap                      Non-interactive setup + fix (zero-config)
1466    status [--json]                Show setup + MCP + rules status
1467    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1468    init --agent <name>            Configure MCP for specific editor/agent
1469    read <file> [-m mode]          Read file with compression
1470    diff <file1> <file2>           Compressed file diff
1471    grep <pattern> [path]          Search with compressed output
1472    find <pattern> [path]          Find files with compressed output
1473    ls [path]                      Directory listing with compression
1474    deps [path]                    Show project dependencies
1475    discover                       Find uncompressed commands in shell history
1476    ghost [--json]                 Ghost Token report: find hidden token waste
1477    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1478    session                        Show adoption statistics
1479    session task <desc>            Set current task
1480    session finding <summary>      Record a finding
1481    session save                   Save current session
1482    session load [id]              Load session (latest if no ID)
1483    knowledge remember <value> --category <c> --key <k>   Store a fact
1484    knowledge recall [query] [--category <c>]             Retrieve facts
1485    knowledge search <query>       Cross-project knowledge search
1486    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1487    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1488    knowledge remove --category <c> --key <k>             Remove a fact
1489    knowledge status               Knowledge base summary
1490    overview [task]                Project overview (task-contextualized if given)
1491    compress [--signatures]        Context compression checkpoint
1492    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1493    profile [list|show|diff|create|set]  Manage context profiles
1494    theme [list|set|export|import] Customize terminal colors and themes
1495    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1496    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1497    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1498    update [--check]               Self-update lean-ctx binary from GitHub Releases
1499    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1500    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1501    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1502    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1503    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1504                                   Code smell detection (Property Graph, 8 rules)
1505    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1506    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1507    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1508    uninstall                      Remove shell hook, MCP configs, and data directory
1509
1510SHELL HOOK PATTERNS (95+):
1511    git       status, log, diff, add, commit, push, pull, fetch, clone,
1512              branch, checkout, switch, merge, stash, tag, reset, remote
1513    docker    build, ps, images, logs, compose, exec, network
1514    npm/pnpm  install, test, run, list, outdated, audit
1515    cargo     build, test, check, clippy
1516    gh        pr list/view/create, issue list/view, run list/view
1517    kubectl   get pods/services/deployments, logs, describe, apply
1518    python    pip install/list/outdated, ruff check/format, poetry, uv
1519    linters   eslint, biome, prettier, golangci-lint
1520    builds    tsc, next build, vite build
1521    ruby      rubocop, bundle install/update, rake test, rails test
1522    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1523    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1524    utils     curl, grep/rg, find, ls, wget, env
1525    data      JSON schema extraction, log deduplication
1526
1527READ MODES:
1528    auto                           Auto-select optimal mode (default)
1529    full                           Full content (cached re-reads = 13 tokens)
1530    map                            Dependency graph + API signatures
1531    signatures                     tree-sitter AST extraction (18 languages)
1532    task                           Task-relevant filtering (requires ctx_session task)
1533    reference                      One-line reference stub (cheap cache key)
1534    aggressive                     Syntax-stripped content
1535    entropy                        Shannon entropy filtered
1536    diff                           Changed lines only
1537    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1538
1539ENVIRONMENT:
1540    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1541    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1542    LEAN_CTX_RAW=1                 Same as --raw for current command
1543    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1544    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1545
1546OPTIONS:
1547    --version, -V                  Show version
1548    --help, -h                     Show this help
1549
1550EXAMPLES:
1551    lean-ctx -c \"git status\"       Compressed git output
1552    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1553    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1554    lean-ctx gain                  Visual terminal dashboard
1555    lean-ctx gain --live           Live auto-updating terminal dashboard
1556    lean-ctx gain --graph          30-day savings chart
1557    lean-ctx gain --daily          Day-by-day breakdown with USD
1558         lean-ctx token-report --json   Machine-readable token + memory report
1559    lean-ctx dashboard             Open web dashboard at localhost:3333
1560    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1561    lean-ctx gain --wrapped        Wrapped report card (recommended)
1562    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1563    lean-ctx sessions list         List all CCP sessions
1564    lean-ctx sessions show         Show latest session state
1565    lean-ctx discover              Find missed savings in shell history
1566    lean-ctx setup                 One-command setup (shell + editors + verify)
1567    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1568    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1569    lean-ctx bootstrap --json      Machine-readable bootstrap report
1570    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1571    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1572    lean-ctx-off                   Disable all shell aliases
1573    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1574    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1575    lean-ctx-mode off              Same as lean-ctx-off
1576    lean-ctx-status                Show whether compression is active
1577    lean-ctx init --agent pi       Install Pi Coding Agent extension
1578    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1579    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1580    lean-ctx doctor --fix --json   Repair + machine-readable report
1581    lean-ctx status --json         Machine-readable current status
1582    lean-ctx session task \"implement auth\"
1583    lean-ctx session finding \"auth.rs:42 — missing validation\"
1584    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1585    lean-ctx knowledge recall \"authentication\"
1586    lean-ctx knowledge search \"database migration\"
1587    lean-ctx overview \"refactor auth module\"
1588    lean-ctx compress --signatures
1589    lean-ctx read src/main.rs -m map
1590    lean-ctx grep \"pub fn\" src/
1591    lean-ctx deps .
1592
1593CLOUD:
1594    cloud status                   Show cloud connection status
1595    login <email>                  Log into existing LeanCTX Cloud account
1596    register <email>               Create a new LeanCTX Cloud account
1597    forgot-password <email>        Send password reset email
1598    sync                           Upload local stats to cloud dashboard
1599    contribute                     Share anonymized compression data
1600
1601TROUBLESHOOTING:
1602    Commands broken?     lean-ctx-off             (fixes current session)
1603    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1604    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1605    Binary missing?      Aliases auto-fallback to original commands (safe)
1606    Preview init?        lean-ctx init --global --dry-run
1607
1608WEBSITE: https://leanctx.com
1609GITHUB:  https://github.com/yvgude/lean-ctx
1610",
1611        version = env!("CARGO_PKG_VERSION"),
1612    );
1613}