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                spawn_proxy_if_needed();
245                run_async(dashboard::start(port, host));
246                return;
247            }
248            "team" => {
249                let sub = rest.first().map_or("help", std::string::String::as_str);
250                match sub {
251                    "serve" => {
252                        #[cfg(feature = "team-server")]
253                        {
254                            let cfg_path = rest
255                                .iter()
256                                .enumerate()
257                                .find_map(|(i, a)| {
258                                    if let Some(v) = a.strip_prefix("--config=") {
259                                        return Some(v.to_string());
260                                    }
261                                    if a == "--config" {
262                                        return rest.get(i + 1).cloned();
263                                    }
264                                    None
265                                })
266                                .unwrap_or_default();
267
268                            if cfg_path.trim().is_empty() {
269                                eprintln!("Usage: lean-ctx team serve --config <path>");
270                                std::process::exit(1);
271                            }
272
273                            let cfg = crate::http_server::team::TeamServerConfig::load(
274                                std::path::Path::new(&cfg_path),
275                            )
276                            .unwrap_or_else(|e| {
277                                eprintln!("Invalid team config: {e}");
278                                std::process::exit(1);
279                            });
280
281                            if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
282                                tracing::error!("Team server error: {e}");
283                                std::process::exit(1);
284                            }
285                            return;
286                        }
287                        #[cfg(not(feature = "team-server"))]
288                        {
289                            eprintln!("lean-ctx team serve is not available in this build");
290                            std::process::exit(1);
291                        }
292                    }
293                    "token" => {
294                        let action = rest.get(1).map_or("help", std::string::String::as_str);
295                        if action == "create" {
296                            #[cfg(feature = "team-server")]
297                            {
298                                let args = &rest[2..];
299                                let cfg_path = args
300                                    .iter()
301                                    .enumerate()
302                                    .find_map(|(i, a)| {
303                                        if let Some(v) = a.strip_prefix("--config=") {
304                                            return Some(v.to_string());
305                                        }
306                                        if a == "--config" {
307                                            return args.get(i + 1).cloned();
308                                        }
309                                        None
310                                    })
311                                    .unwrap_or_default();
312                                let token_id = args
313                                    .iter()
314                                    .enumerate()
315                                    .find_map(|(i, a)| {
316                                        if let Some(v) = a.strip_prefix("--id=") {
317                                            return Some(v.to_string());
318                                        }
319                                        if a == "--id" {
320                                            return args.get(i + 1).cloned();
321                                        }
322                                        None
323                                    })
324                                    .unwrap_or_default();
325                                let scopes_csv = args
326                                    .iter()
327                                    .enumerate()
328                                    .find_map(|(i, a)| {
329                                        if let Some(v) = a.strip_prefix("--scopes=") {
330                                            return Some(v.to_string());
331                                        }
332                                        if let Some(v) = a.strip_prefix("--scope=") {
333                                            return Some(v.to_string());
334                                        }
335                                        if a == "--scopes" || a == "--scope" {
336                                            return args.get(i + 1).cloned();
337                                        }
338                                        None
339                                    })
340                                    .unwrap_or_default();
341
342                                if cfg_path.trim().is_empty()
343                                    || token_id.trim().is_empty()
344                                    || scopes_csv.trim().is_empty()
345                                {
346                                    eprintln!(
347                                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
348                                        );
349                                    std::process::exit(1);
350                                }
351
352                                let cfg_p = std::path::PathBuf::from(&cfg_path);
353                                let mut cfg = crate::http_server::team::TeamServerConfig::load(
354                                    cfg_p.as_path(),
355                                )
356                                .unwrap_or_else(|e| {
357                                    eprintln!("Invalid team config: {e}");
358                                    std::process::exit(1);
359                                });
360
361                                let mut scopes = Vec::new();
362                                for part in scopes_csv.split(',') {
363                                    let p = part.trim().to_ascii_lowercase();
364                                    if p.is_empty() {
365                                        continue;
366                                    }
367                                    let scope = match p.as_str() {
368                                        "search" => crate::http_server::team::TeamScope::Search,
369                                        "graph" => crate::http_server::team::TeamScope::Graph,
370                                        "artifacts" => {
371                                            crate::http_server::team::TeamScope::Artifacts
372                                        }
373                                        "index" => crate::http_server::team::TeamScope::Index,
374                                        "events" => crate::http_server::team::TeamScope::Events,
375                                        "sessionmutations" | "session_mutations" => {
376                                            crate::http_server::team::TeamScope::SessionMutations
377                                        }
378                                        "knowledge" => {
379                                            crate::http_server::team::TeamScope::Knowledge
380                                        }
381                                        "audit" => crate::http_server::team::TeamScope::Audit,
382                                        _ => {
383                                            eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
384                                            std::process::exit(1);
385                                        }
386                                    };
387                                    if !scopes.contains(&scope) {
388                                        scopes.push(scope);
389                                    }
390                                }
391                                if scopes.is_empty() {
392                                    eprintln!("At least 1 scope is required");
393                                    std::process::exit(1);
394                                }
395
396                                let (token, hash) = crate::http_server::team::create_token()
397                                    .unwrap_or_else(|e| {
398                                        eprintln!("Token generation failed: {e}");
399                                        std::process::exit(1);
400                                    });
401
402                                cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
403                                    id: token_id,
404                                    sha256_hex: hash,
405                                    scopes,
406                                });
407
408                                cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
409                                    eprintln!("Failed to write config: {e}");
410                                    std::process::exit(1);
411                                });
412
413                                println!("{token}");
414                                return;
415                            }
416
417                            #[cfg(not(feature = "team-server"))]
418                            {
419                                eprintln!("lean-ctx team token is not available in this build");
420                                std::process::exit(1);
421                            }
422                        }
423                        eprintln!(
424                            "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
425                        );
426                        std::process::exit(1);
427                    }
428                    "sync" => {
429                        #[cfg(feature = "team-server")]
430                        {
431                            let args = &rest[1..];
432                            let cfg_path = args
433                                .iter()
434                                .enumerate()
435                                .find_map(|(i, a)| {
436                                    if let Some(v) = a.strip_prefix("--config=") {
437                                        return Some(v.to_string());
438                                    }
439                                    if a == "--config" {
440                                        return args.get(i + 1).cloned();
441                                    }
442                                    None
443                                })
444                                .unwrap_or_default();
445                            if cfg_path.trim().is_empty() {
446                                eprintln!(
447                                    "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
448                                );
449                                std::process::exit(1);
450                            }
451                            let only_ws = args.iter().enumerate().find_map(|(i, a)| {
452                                if let Some(v) = a.strip_prefix("--workspace=") {
453                                    return Some(v.to_string());
454                                }
455                                if let Some(v) = a.strip_prefix("--workspace-id=") {
456                                    return Some(v.to_string());
457                                }
458                                if a == "--workspace" || a == "--workspace-id" {
459                                    return args.get(i + 1).cloned();
460                                }
461                                None
462                            });
463
464                            let cfg = crate::http_server::team::TeamServerConfig::load(
465                                std::path::Path::new(&cfg_path),
466                            )
467                            .unwrap_or_else(|e| {
468                                eprintln!("Invalid team config: {e}");
469                                std::process::exit(1);
470                            });
471
472                            for ws in &cfg.workspaces {
473                                if let Some(ref only) = only_ws {
474                                    if ws.id != *only {
475                                        continue;
476                                    }
477                                }
478                                let git_dir = ws.root.join(".git");
479                                if !git_dir.exists() {
480                                    eprintln!(
481                                        "workspace '{}' root is not a git repo: {}",
482                                        ws.id,
483                                        ws.root.display()
484                                    );
485                                    std::process::exit(1);
486                                }
487                                let status = std::process::Command::new("git")
488                                    .arg("-C")
489                                    .arg(&ws.root)
490                                    .args(["fetch", "--all", "--prune"])
491                                    .status()
492                                    .unwrap_or_else(|e| {
493                                        eprintln!(
494                                            "git fetch failed for workspace '{}': {e}",
495                                            ws.id
496                                        );
497                                        std::process::exit(1);
498                                    });
499                                if !status.success() {
500                                    eprintln!(
501                                        "git fetch failed for workspace '{}' (exit={})",
502                                        ws.id,
503                                        status.code().unwrap_or(1)
504                                    );
505                                    std::process::exit(1);
506                                }
507                            }
508                            return;
509                        }
510                        #[cfg(not(feature = "team-server"))]
511                        {
512                            eprintln!("lean-ctx team sync is not available in this build");
513                            std::process::exit(1);
514                        }
515                    }
516                    _ => {
517                        eprintln!(
518                            "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>]"
519                        );
520                        std::process::exit(1);
521                    }
522                }
523            }
524            "serve" => {
525                #[cfg(feature = "http-server")]
526                {
527                    let mut cfg = crate::http_server::HttpServerConfig::default();
528                    let mut daemon_mode = false;
529                    let mut stop_mode = false;
530                    let mut status_mode = false;
531                    let mut foreground_daemon = false;
532                    let mut i = 0;
533                    while i < rest.len() {
534                        match rest[i].as_str() {
535                            "--daemon" | "-d" => daemon_mode = true,
536                            "--stop" => stop_mode = true,
537                            "--status" => status_mode = true,
538                            "--_foreground-daemon" => foreground_daemon = true,
539                            "--host" | "-H" => {
540                                i += 1;
541                                if i < rest.len() {
542                                    cfg.host.clone_from(&rest[i]);
543                                }
544                            }
545                            arg if arg.starts_with("--host=") => {
546                                cfg.host = arg["--host=".len()..].to_string();
547                            }
548                            "--port" | "-p" => {
549                                i += 1;
550                                if i < rest.len() {
551                                    if let Ok(p) = rest[i].parse::<u16>() {
552                                        cfg.port = p;
553                                    }
554                                }
555                            }
556                            arg if arg.starts_with("--port=") => {
557                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
558                                    cfg.port = p;
559                                }
560                            }
561                            "--project-root" => {
562                                i += 1;
563                                if i < rest.len() {
564                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
565                                }
566                            }
567                            arg if arg.starts_with("--project-root=") => {
568                                cfg.project_root =
569                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
570                            }
571                            "--auth-token" => {
572                                i += 1;
573                                if i < rest.len() {
574                                    cfg.auth_token = Some(rest[i].clone());
575                                }
576                            }
577                            arg if arg.starts_with("--auth-token=") => {
578                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
579                            }
580                            "--stateful" => cfg.stateful_mode = true,
581                            "--stateless" => cfg.stateful_mode = false,
582                            "--json" => cfg.json_response = true,
583                            "--sse" => cfg.json_response = false,
584                            "--disable-host-check" => cfg.disable_host_check = true,
585                            "--allowed-host" => {
586                                i += 1;
587                                if i < rest.len() {
588                                    cfg.allowed_hosts.push(rest[i].clone());
589                                }
590                            }
591                            arg if arg.starts_with("--allowed-host=") => {
592                                cfg.allowed_hosts
593                                    .push(arg["--allowed-host=".len()..].to_string());
594                            }
595                            "--max-body-bytes" => {
596                                i += 1;
597                                if i < rest.len() {
598                                    if let Ok(n) = rest[i].parse::<usize>() {
599                                        cfg.max_body_bytes = n;
600                                    }
601                                }
602                            }
603                            arg if arg.starts_with("--max-body-bytes=") => {
604                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
605                                    cfg.max_body_bytes = n;
606                                }
607                            }
608                            "--max-concurrency" => {
609                                i += 1;
610                                if i < rest.len() {
611                                    if let Ok(n) = rest[i].parse::<usize>() {
612                                        cfg.max_concurrency = n;
613                                    }
614                                }
615                            }
616                            arg if arg.starts_with("--max-concurrency=") => {
617                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
618                                    cfg.max_concurrency = n;
619                                }
620                            }
621                            "--max-rps" => {
622                                i += 1;
623                                if i < rest.len() {
624                                    if let Ok(n) = rest[i].parse::<u32>() {
625                                        cfg.max_rps = n;
626                                    }
627                                }
628                            }
629                            arg if arg.starts_with("--max-rps=") => {
630                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
631                                    cfg.max_rps = n;
632                                }
633                            }
634                            "--rate-burst" => {
635                                i += 1;
636                                if i < rest.len() {
637                                    if let Ok(n) = rest[i].parse::<u32>() {
638                                        cfg.rate_burst = n;
639                                    }
640                                }
641                            }
642                            arg if arg.starts_with("--rate-burst=") => {
643                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
644                                    cfg.rate_burst = n;
645                                }
646                            }
647                            "--request-timeout-ms" => {
648                                i += 1;
649                                if i < rest.len() {
650                                    if let Ok(n) = rest[i].parse::<u64>() {
651                                        cfg.request_timeout_ms = n;
652                                    }
653                                }
654                            }
655                            arg if arg.starts_with("--request-timeout-ms=") => {
656                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
657                                    cfg.request_timeout_ms = n;
658                                }
659                            }
660                            "--help" | "-h" => {
661                                eprintln!(
662                                    "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
663                                     \\n\\
664                                     Options:\\n\\
665                                       --daemon, -d          Start as background daemon (UDS)\\n\\
666                                       --stop                Stop running daemon\\n\\
667                                       --status              Show daemon status\\n\\
668                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
669                                       --port, -p            Bind port (default: 8080)\\n\\
670                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
671                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
672                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
673                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
674                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
675                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
676                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
677                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
678                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
679                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
680                                       --disable-host-check  Disable Host header validation (unsafe)"
681                                );
682                                return;
683                            }
684                            _ => {}
685                        }
686                        i += 1;
687                    }
688
689                    if stop_mode {
690                        if let Err(e) = crate::daemon::stop_daemon() {
691                            eprintln!("Error: {e}");
692                            std::process::exit(1);
693                        }
694                        return;
695                    }
696
697                    if status_mode {
698                        println!("{}", crate::daemon::daemon_status());
699                        return;
700                    }
701
702                    if daemon_mode {
703                        if let Err(e) = crate::daemon::start_daemon(&rest) {
704                            eprintln!("Error: {e}");
705                            std::process::exit(1);
706                        }
707                        return;
708                    }
709
710                    if foreground_daemon {
711                        if let Err(e) = crate::daemon::init_foreground_daemon() {
712                            eprintln!("Error writing PID file: {e}");
713                            std::process::exit(1);
714                        }
715                        let addr = crate::daemon::daemon_addr();
716                        if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
717                        {
718                            tracing::error!("Daemon server error: {e}");
719                            crate::daemon::cleanup_daemon_files();
720                            std::process::exit(1);
721                        }
722                        crate::daemon::cleanup_daemon_files();
723                        return;
724                    }
725
726                    if cfg.auth_token.is_none() {
727                        if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
728                            if !v.trim().is_empty() {
729                                cfg.auth_token = Some(v);
730                            }
731                        }
732                    }
733
734                    if let Err(e) = run_async(crate::http_server::serve(cfg)) {
735                        tracing::error!("HTTP server error: {e}");
736                        std::process::exit(1);
737                    }
738                    return;
739                }
740                #[cfg(not(feature = "http-server"))]
741                {
742                    eprintln!("lean-ctx serve is not available in this build");
743                    std::process::exit(1);
744                }
745            }
746            "watch" => {
747                if rest.iter().any(|a| a == "--help" || a == "-h") {
748                    println!("Usage: lean-ctx watch");
749                    println!("  Live TUI dashboard (real-time event stream).");
750                    return;
751                }
752                if let Err(e) = tui::run() {
753                    tracing::error!("TUI error: {e}");
754                    std::process::exit(1);
755                }
756                return;
757            }
758            "proxy" => {
759                #[cfg(feature = "http-server")]
760                {
761                    let sub = rest.first().map_or("help", std::string::String::as_str);
762                    match sub {
763                        "start" => {
764                            let port: u16 = rest
765                                .iter()
766                                .find_map(|p| {
767                                    p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
768                                })
769                                .and_then(|p| p.parse().ok())
770                                .unwrap_or(4444);
771                            let autostart = rest.iter().any(|a| a == "--autostart");
772                            if autostart {
773                                crate::proxy_autostart::install(port, false);
774                                return;
775                            }
776                            if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
777                                tracing::error!("Proxy error: {e}");
778                                std::process::exit(1);
779                            }
780                        }
781                        "stop" => {
782                            let port: u16 = rest
783                                .iter()
784                                .find_map(|p| p.strip_prefix("--port="))
785                                .and_then(|p| p.parse().ok())
786                                .unwrap_or(4444);
787                            let health_url = format!("http://127.0.0.1:{port}/health");
788                            match ureq::get(&health_url).call() {
789                                Ok(resp) => {
790                                    if let Ok(body) = resp.into_body().read_to_string() {
791                                        if let Some(pid_str) = body
792                                            .split("pid\":")
793                                            .nth(1)
794                                            .and_then(|s| s.split([',', '}']).next())
795                                        {
796                                            if let Ok(pid) = pid_str.trim().parse::<u32>() {
797                                                let _ =
798                                                    crate::ipc::process::terminate_gracefully(pid);
799                                                std::thread::sleep(
800                                                    std::time::Duration::from_millis(500),
801                                                );
802                                                if crate::ipc::process::is_alive(pid) {
803                                                    let _ = crate::ipc::process::force_kill(pid);
804                                                }
805                                                println!(
806                                                    "Proxy on port {port} stopped (PID {pid})."
807                                                );
808                                                return;
809                                            }
810                                        }
811                                    }
812                                    println!("Proxy on port {port} running but could not parse PID. Use `lean-ctx stop` to kill all.");
813                                }
814                                Err(_) => {
815                                    println!("No proxy running on port {port}.");
816                                }
817                            }
818                        }
819                        "status" => {
820                            let port: u16 = rest
821                                .iter()
822                                .find_map(|p| p.strip_prefix("--port="))
823                                .and_then(|p| p.parse().ok())
824                                .unwrap_or(4444);
825                            if let Ok(resp) =
826                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
827                            {
828                                let body = resp.into_body().read_to_string().unwrap_or_default();
829                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
830                                    println!("lean-ctx proxy status:");
831                                    println!("  Requests:    {}", v["requests_total"]);
832                                    println!("  Compressed:  {}", v["requests_compressed"]);
833                                    println!("  Tokens saved: {}", v["tokens_saved"]);
834                                    println!(
835                                        "  Compression: {}%",
836                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
837                                    );
838                                } else {
839                                    println!("{body}");
840                                }
841                            } else {
842                                println!("No proxy running on port {port}.");
843                                println!("Start with: lean-ctx proxy start");
844                            }
845                        }
846                        _ => {
847                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
848                        }
849                    }
850                    return;
851                }
852                #[cfg(not(feature = "http-server"))]
853                {
854                    eprintln!("lean-ctx proxy is not available in this build");
855                    std::process::exit(1);
856                }
857            }
858            "init" => {
859                super::cmd_init(&rest);
860                return;
861            }
862            "setup" => {
863                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
864                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
865                let fix = rest.iter().any(|a| a == "--fix");
866                let json = rest.iter().any(|a| a == "--json");
867                let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
868
869                if non_interactive || fix || json || yes {
870                    let opts = setup::SetupOptions {
871                        non_interactive,
872                        yes,
873                        fix,
874                        json,
875                        no_auto_approve,
876                    };
877                    match setup::run_setup_with_options(opts) {
878                        Ok(report) => {
879                            if json {
880                                println!(
881                                    "{}",
882                                    serde_json::to_string_pretty(&report)
883                                        .unwrap_or_else(|_| "{}".to_string())
884                                );
885                            }
886                            if !report.success {
887                                std::process::exit(1);
888                            }
889                        }
890                        Err(e) => {
891                            eprintln!("{e}");
892                            std::process::exit(1);
893                        }
894                    }
895                } else {
896                    setup::run_setup();
897                }
898                return;
899            }
900            "install" => {
901                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
902                let json = rest.iter().any(|a| a == "--json");
903                if !repair {
904                    eprintln!("Usage: lean-ctx install --repair [--json]");
905                    std::process::exit(1);
906                }
907                let opts = setup::SetupOptions {
908                    non_interactive: true,
909                    yes: true,
910                    fix: true,
911                    json,
912                    ..Default::default()
913                };
914                match setup::run_setup_with_options(opts) {
915                    Ok(report) => {
916                        if json {
917                            println!(
918                                "{}",
919                                serde_json::to_string_pretty(&report)
920                                    .unwrap_or_else(|_| "{}".to_string())
921                            );
922                        }
923                        if !report.success {
924                            std::process::exit(1);
925                        }
926                    }
927                    Err(e) => {
928                        eprintln!("{e}");
929                        std::process::exit(1);
930                    }
931                }
932                return;
933            }
934            "bootstrap" => {
935                let json = rest.iter().any(|a| a == "--json");
936                let opts = setup::SetupOptions {
937                    non_interactive: true,
938                    yes: true,
939                    fix: true,
940                    json,
941                    ..Default::default()
942                };
943                match setup::run_setup_with_options(opts) {
944                    Ok(report) => {
945                        if json {
946                            println!(
947                                "{}",
948                                serde_json::to_string_pretty(&report)
949                                    .unwrap_or_else(|_| "{}".to_string())
950                            );
951                        }
952                        if !report.success {
953                            std::process::exit(1);
954                        }
955                    }
956                    Err(e) => {
957                        eprintln!("{e}");
958                        std::process::exit(1);
959                    }
960                }
961                return;
962            }
963            "status" => {
964                let code = status::run_cli(&rest);
965                if code != 0 {
966                    std::process::exit(code);
967                }
968                return;
969            }
970            "read" => {
971                super::cmd_read(&rest);
972                core::stats::flush();
973                return;
974            }
975            "diff" => {
976                super::cmd_diff(&rest);
977                core::stats::flush();
978                return;
979            }
980            "grep" => {
981                super::cmd_grep(&rest);
982                core::stats::flush();
983                return;
984            }
985            "find" => {
986                super::cmd_find(&rest);
987                core::stats::flush();
988                return;
989            }
990            "ls" => {
991                super::cmd_ls(&rest);
992                core::stats::flush();
993                return;
994            }
995            "deps" => {
996                super::cmd_deps(&rest);
997                core::stats::flush();
998                return;
999            }
1000            "discover" => {
1001                super::cmd_discover(&rest);
1002                return;
1003            }
1004            "ghost" => {
1005                super::cmd_ghost(&rest);
1006                return;
1007            }
1008            "filter" => {
1009                super::cmd_filter(&rest);
1010                return;
1011            }
1012            "heatmap" => {
1013                heatmap::cmd_heatmap(&rest);
1014                return;
1015            }
1016            "graph" => {
1017                let sub = rest.first().map_or("build", std::string::String::as_str);
1018                match sub {
1019                    "build" => {
1020                        let root = rest.get(1).cloned().or_else(|| {
1021                            std::env::current_dir()
1022                                .ok()
1023                                .map(|p| p.to_string_lossy().to_string())
1024                        });
1025                        let root = root.unwrap_or_else(|| ".".to_string());
1026                        let index = core::graph_index::load_or_build(&root);
1027                        println!(
1028                            "Graph built: {} files, {} edges",
1029                            index.files.len(),
1030                            index.edges.len()
1031                        );
1032                    }
1033                    "export-html" => {
1034                        let mut root: Option<String> = None;
1035                        let mut out: Option<String> = None;
1036                        let mut max_nodes: usize = 2500;
1037
1038                        let args = &rest[1..];
1039                        let mut i = 0usize;
1040                        while i < args.len() {
1041                            let a = args[i].as_str();
1042                            if let Some(v) = a.strip_prefix("--root=") {
1043                                root = Some(v.to_string());
1044                            } else if a == "--root" {
1045                                root = args.get(i + 1).cloned();
1046                                i += 1;
1047                            } else if let Some(v) = a.strip_prefix("--out=") {
1048                                out = Some(v.to_string());
1049                            } else if a == "--out" {
1050                                out = args.get(i + 1).cloned();
1051                                i += 1;
1052                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1053                                max_nodes = v.parse::<usize>().unwrap_or(0);
1054                            } else if a == "--max-nodes" {
1055                                let v = args.get(i + 1).map_or("", String::as_str);
1056                                max_nodes = v.parse::<usize>().unwrap_or(0);
1057                                i += 1;
1058                            }
1059                            i += 1;
1060                        }
1061
1062                        let root = root
1063                            .or_else(|| {
1064                                std::env::current_dir()
1065                                    .ok()
1066                                    .map(|p| p.to_string_lossy().to_string())
1067                            })
1068                            .unwrap_or_else(|| ".".to_string());
1069                        let Some(out) = out else {
1070                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1071                            std::process::exit(1);
1072                        };
1073                        if max_nodes == 0 {
1074                            eprintln!("--max-nodes must be >= 1");
1075                            std::process::exit(1);
1076                        }
1077
1078                        core::graph_export::export_graph_html(
1079                            &root,
1080                            std::path::Path::new(&out),
1081                            max_nodes,
1082                        )
1083                        .unwrap_or_else(|e| {
1084                            eprintln!("graph export failed: {e}");
1085                            std::process::exit(1);
1086                        });
1087                        println!("{out}");
1088                    }
1089                    _ => {
1090                        eprintln!(
1091                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1092                        );
1093                        std::process::exit(1);
1094                    }
1095                }
1096                return;
1097            }
1098            "smells" => {
1099                let action = rest.first().map_or("summary", String::as_str);
1100                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1101                    if let Some(v) = a.strip_prefix("--rule=") {
1102                        return Some(v.to_string());
1103                    }
1104                    if a == "--rule" {
1105                        return rest.get(i + 1).cloned();
1106                    }
1107                    None
1108                });
1109                let path = rest.iter().enumerate().find_map(|(i, a)| {
1110                    if let Some(v) = a.strip_prefix("--path=") {
1111                        return Some(v.to_string());
1112                    }
1113                    if a == "--path" {
1114                        return rest.get(i + 1).cloned();
1115                    }
1116                    None
1117                });
1118                let root = rest
1119                    .iter()
1120                    .enumerate()
1121                    .find_map(|(i, a)| {
1122                        if let Some(v) = a.strip_prefix("--root=") {
1123                            return Some(v.to_string());
1124                        }
1125                        if a == "--root" {
1126                            return rest.get(i + 1).cloned();
1127                        }
1128                        None
1129                    })
1130                    .or_else(|| {
1131                        std::env::current_dir()
1132                            .ok()
1133                            .map(|p| p.to_string_lossy().to_string())
1134                    })
1135                    .unwrap_or_else(|| ".".to_string());
1136                let fmt = if rest.iter().any(|a| a == "--json") {
1137                    Some("json")
1138                } else {
1139                    None
1140                };
1141                println!(
1142                    "{}",
1143                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1144                );
1145                return;
1146            }
1147            "session" => {
1148                super::cmd_session_action(&rest);
1149                return;
1150            }
1151            "control" | "context-control" => {
1152                super::cmd_control(&rest);
1153                return;
1154            }
1155            "plan" | "context-plan" => {
1156                super::cmd_plan(&rest);
1157                return;
1158            }
1159            "compile" | "context-compile" => {
1160                super::cmd_compile(&rest);
1161                return;
1162            }
1163            "knowledge" => {
1164                super::cmd_knowledge(&rest);
1165                return;
1166            }
1167            "overview" => {
1168                super::cmd_overview(&rest);
1169                return;
1170            }
1171            "compress" => {
1172                super::cmd_compress(&rest);
1173                return;
1174            }
1175            "wrapped" => {
1176                super::cmd_wrapped(&rest);
1177                return;
1178            }
1179            "sessions" => {
1180                super::cmd_sessions(&rest);
1181                return;
1182            }
1183            "benchmark" => {
1184                super::cmd_benchmark(&rest);
1185                return;
1186            }
1187            "profile" => {
1188                super::cmd_profile(&rest);
1189                return;
1190            }
1191            "config" => {
1192                super::cmd_config(&rest);
1193                return;
1194            }
1195            "stats" => {
1196                super::cmd_stats(&rest);
1197                return;
1198            }
1199            "cache" => {
1200                super::cmd_cache(&rest);
1201                return;
1202            }
1203            "theme" => {
1204                super::cmd_theme(&rest);
1205                return;
1206            }
1207            "tee" => {
1208                super::cmd_tee(&rest);
1209                return;
1210            }
1211            "terse" | "compression" => {
1212                super::cmd_compression(&rest);
1213                return;
1214            }
1215            "slow-log" => {
1216                super::cmd_slow_log(&rest);
1217                return;
1218            }
1219            "update" | "--self-update" => {
1220                core::updater::run(&rest);
1221                return;
1222            }
1223            "restart" => {
1224                cmd_restart();
1225                return;
1226            }
1227            "stop" => {
1228                cmd_stop();
1229                return;
1230            }
1231            "dev-install" => {
1232                cmd_dev_install();
1233                return;
1234            }
1235            "doctor" => {
1236                let code = doctor::run_cli(&rest);
1237                if code != 0 {
1238                    std::process::exit(code);
1239                }
1240                return;
1241            }
1242            "gotchas" | "bugs" => {
1243                super::cloud::cmd_gotchas(&rest);
1244                return;
1245            }
1246            "learn" => {
1247                super::cmd_learn(&rest);
1248                return;
1249            }
1250            "buddy" | "pet" => {
1251                super::cloud::cmd_buddy(&rest);
1252                return;
1253            }
1254            "hook" => {
1255                hook_handlers::mark_hook_environment();
1256                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1257                let action = rest.first().map_or("help", std::string::String::as_str);
1258                match action {
1259                    "rewrite" => hook_handlers::handle_rewrite(),
1260                    "redirect" => hook_handlers::handle_redirect(),
1261                    "observe" => hook_handlers::handle_observe(),
1262                    "copilot" => hook_handlers::handle_copilot(),
1263                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1264                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1265                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1266                    _ => {
1267                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1268                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1269                        std::process::exit(1);
1270                    }
1271                }
1272                return;
1273            }
1274            "report-issue" | "report" => {
1275                report::run(&rest);
1276                return;
1277            }
1278            "uninstall" => {
1279                let dry_run = rest.iter().any(|a| a == "--dry-run");
1280                uninstall::run(dry_run);
1281                return;
1282            }
1283            "bypass" => {
1284                if rest.is_empty() {
1285                    eprintln!("Usage: lean-ctx bypass \"command\"");
1286                    eprintln!("Runs the command with zero compression (raw passthrough).");
1287                    std::process::exit(1);
1288                }
1289                let command = if rest.len() == 1 {
1290                    rest[0].clone()
1291                } else {
1292                    shell::join_command(&args[2..])
1293                };
1294                std::env::set_var("LEAN_CTX_RAW", "1");
1295                let code = shell::exec(&command);
1296                std::process::exit(code);
1297            }
1298            "safety-levels" | "safety" => {
1299                println!("{}", core::compression_safety::format_safety_table());
1300                return;
1301            }
1302            "cheat" | "cheatsheet" | "cheat-sheet" => {
1303                super::cmd_cheatsheet();
1304                return;
1305            }
1306            "login" => {
1307                super::cloud::cmd_login(&rest);
1308                return;
1309            }
1310            "register" => {
1311                super::cloud::cmd_register(&rest);
1312                return;
1313            }
1314            "forgot-password" => {
1315                super::cloud::cmd_forgot_password(&rest);
1316                return;
1317            }
1318            "sync" => {
1319                super::cloud::cmd_sync();
1320                return;
1321            }
1322            "contribute" => {
1323                super::cloud::cmd_contribute();
1324                return;
1325            }
1326            "cloud" => {
1327                super::cloud::cmd_cloud(&rest);
1328                return;
1329            }
1330            "upgrade" => {
1331                super::cloud::cmd_upgrade();
1332                return;
1333            }
1334            "--version" | "-V" => {
1335                println!("{}", core::integrity::origin_line());
1336                return;
1337            }
1338            "--help" | "-h" => {
1339                print_help();
1340                return;
1341            }
1342            "mcp" => {}
1343            _ => {
1344                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1345                print_help();
1346                std::process::exit(1);
1347            }
1348        }
1349    }
1350
1351    if let Err(e) = run_mcp_server() {
1352        tracing::error!("lean-ctx: {e}");
1353        std::process::exit(1);
1354    }
1355}
1356
1357fn passthrough(command: &str) -> ! {
1358    let (shell, flag) = shell::shell_and_flag();
1359    let status = std::process::Command::new(&shell)
1360        .arg(&flag)
1361        .arg(command)
1362        .env("LEAN_CTX_ACTIVE", "1")
1363        .status()
1364        .map_or(127, |s| s.code().unwrap_or(1));
1365    std::process::exit(status);
1366}
1367
1368fn run_async<F: std::future::Future>(future: F) -> F::Output {
1369    tokio::runtime::Runtime::new()
1370        .expect("failed to create async runtime")
1371        .block_on(future)
1372}
1373
1374fn run_mcp_server() -> Result<()> {
1375    use rmcp::ServiceExt;
1376
1377    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1378
1379    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1380
1381    // Concurrency hardening:
1382    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1383    // - Limits Tokio worker/blocking threads to avoid host degradation.
1384    // - LEAN_CTX_WORKER_THREADS overrides the default for environments
1385    //   with many concurrent subagents (e.g. parallel review pipelines).
1386    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1387        "mcp-startup",
1388        std::time::Duration::from_secs(3),
1389        std::time::Duration::from_secs(30),
1390    );
1391
1392    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1393    let worker_threads = resolve_worker_threads(parallelism);
1394    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1395
1396    let rt = tokio::runtime::Builder::new_multi_thread()
1397        .worker_threads(worker_threads)
1398        .max_blocking_threads(max_blocking_threads)
1399        .enable_all()
1400        .build()?;
1401
1402    let server = tools::create_server();
1403    drop(startup_lock);
1404
1405    // Auto-start proxy in background so the dashboard gets exact token data.
1406    spawn_proxy_if_needed();
1407
1408    rt.block_on(async {
1409        core::logging::init_mcp_logging();
1410        core::protocol::set_mcp_context(true);
1411
1412        tracing::info!(
1413            "lean-ctx v{} MCP server starting",
1414            env!("CARGO_PKG_VERSION")
1415        );
1416
1417        let transport =
1418            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1419        let server_handle = server.clone();
1420        let service = match server.serve(transport).await {
1421            Ok(s) => s,
1422            Err(e) => {
1423                let msg = e.to_string();
1424                if msg.contains("expect initialized")
1425                    || msg.contains("context canceled")
1426                    || msg.contains("broken pipe")
1427                {
1428                    tracing::debug!("Client disconnected before init: {msg}");
1429                    return Ok(());
1430                }
1431                return Err(e.into());
1432            }
1433        };
1434        match service.waiting().await {
1435            Ok(reason) => {
1436                tracing::info!("MCP server stopped: {reason:?}");
1437            }
1438            Err(e) => {
1439                let msg = e.to_string();
1440                if msg.contains("broken pipe")
1441                    || msg.contains("connection reset")
1442                    || msg.contains("context canceled")
1443                {
1444                    tracing::info!("MCP server: transport closed ({msg})");
1445                } else {
1446                    tracing::error!("MCP server error: {msg}");
1447                }
1448            }
1449        }
1450
1451        server_handle.shutdown().await;
1452
1453        core::stats::flush();
1454        core::heatmap::flush();
1455        core::mode_predictor::ModePredictor::flush();
1456        core::feedback::FeedbackStore::flush();
1457
1458        Ok(())
1459    })
1460}
1461
1462fn print_help() {
1463    println!(
1464        "lean-ctx {version} — Context Runtime for AI Agents
1465
146660+ compression patterns | 51 MCP tools | 10 read modes | Context Continuity Protocol
1467
1468USAGE:
1469    lean-ctx                       Start MCP server (stdio)
1470    lean-ctx serve                 Start MCP server (Streamable HTTP)
1471    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1472    lean-ctx serve --stop          Stop running daemon
1473    lean-ctx serve --status        Show daemon status
1474    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1475    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1476    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1477    lean-ctx exec \"command\"        Same as -c
1478    lean-ctx shell                 Interactive shell with compression
1479
1480COMMANDS:
1481    gain                           Visual dashboard (colors, bars, sparklines, USD)
1482    gain --live                    Live mode: auto-refreshes every 1s in-place
1483    gain --graph                   30-day savings chart
1484    gain --daily                   Bordered day-by-day table with USD
1485    gain --json                    Raw JSON export of all stats
1486         token-report [--json]          Token + memory report (project + session + CEP)
1487    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1488    index <status|build|build-full|watch>  Codebase index utilities
1489    cep                            CEP impact report (score trends, cache, modes)
1490    watch                          Live TUI dashboard (real-time event stream)
1491    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1492    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1493    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1494    proxy status                   Show proxy statistics
1495    cache [list|clear|stats]       Show/manage file read cache
1496    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1497    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1498    benchmark run [path] [--json]  Run real benchmark on project files
1499    benchmark report [path]        Generate shareable Markdown report
1500    cheatsheet                     Command cheat sheet & workflow quick reference
1501    setup                          One-command setup: shell + editor + verify
1502    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1503    bootstrap                      Non-interactive setup + fix (zero-config)
1504    status [--json]                Show setup + MCP + rules status
1505    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1506    init --agent <name>            Configure MCP for specific editor/agent
1507    read <file> [-m mode]          Read file with compression
1508    diff <file1> <file2>           Compressed file diff
1509    grep <pattern> [path]          Search with compressed output
1510    find <pattern> [path]          Find files with compressed output
1511    ls [path]                      Directory listing with compression
1512    deps [path]                    Show project dependencies
1513    discover                       Find uncompressed commands in shell history
1514    ghost [--json]                 Ghost Token report: find hidden token waste
1515    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1516    session                        Show adoption statistics
1517    session task <desc>            Set current task
1518    session finding <summary>      Record a finding
1519    session save                   Save current session
1520    session load [id]              Load session (latest if no ID)
1521    knowledge remember <value> --category <c> --key <k>   Store a fact
1522    knowledge recall [query] [--category <c>]             Retrieve facts
1523    knowledge search <query>       Cross-project knowledge search
1524    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1525    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1526    knowledge remove --category <c> --key <k>             Remove a fact
1527    knowledge status               Knowledge base summary
1528    overview [task]                Project overview (task-contextualized if given)
1529    compress [--signatures]        Context compression checkpoint
1530    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1531    profile [list|show|diff|create|set]  Manage context profiles
1532    theme [list|set|export|import] Customize terminal colors and themes
1533    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1534    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1535    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1536    update [--check]               Self-update lean-ctx binary from GitHub Releases
1537    stop                           Stop ALL lean-ctx processes (daemon, proxy, orphans)
1538    restart                        Restart daemon (applies config.toml changes)
1539    dev-install                    Build release + atomic install + restart (for development)
1540    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1541    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1542    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1543    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1544    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1545                                   Code smell detection (Property Graph, 8 rules)
1546    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1547    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1548    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1549    uninstall                      Remove shell hook, MCP configs, and data directory
1550
1551SHELL HOOK PATTERNS (95+):
1552    git       status, log, diff, add, commit, push, pull, fetch, clone,
1553              branch, checkout, switch, merge, stash, tag, reset, remote
1554    docker    build, ps, images, logs, compose, exec, network
1555    npm/pnpm  install, test, run, list, outdated, audit
1556    cargo     build, test, check, clippy
1557    gh        pr list/view/create, issue list/view, run list/view
1558    kubectl   get pods/services/deployments, logs, describe, apply
1559    python    pip install/list/outdated, ruff check/format, poetry, uv
1560    linters   eslint, biome, prettier, golangci-lint
1561    builds    tsc, next build, vite build
1562    ruby      rubocop, bundle install/update, rake test, rails test
1563    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1564    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1565    utils     curl, grep/rg, find, ls, wget, env
1566    data      JSON schema extraction, log deduplication
1567
1568READ MODES:
1569    auto                           Auto-select optimal mode (default)
1570    full                           Full content (cached re-reads = 13 tokens)
1571    map                            Dependency graph + API signatures
1572    signatures                     tree-sitter AST extraction (18 languages)
1573    task                           Task-relevant filtering (requires ctx_session task)
1574    reference                      One-line reference stub (cheap cache key)
1575    aggressive                     Syntax-stripped content
1576    entropy                        Shannon entropy filtered
1577    diff                           Changed lines only
1578    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1579
1580ENVIRONMENT:
1581    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1582    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1583    LEAN_CTX_RAW=1                 Same as --raw for current command
1584    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1585    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1586
1587OPTIONS:
1588    --version, -V                  Show version
1589    --help, -h                     Show this help
1590
1591EXAMPLES:
1592    lean-ctx -c \"git status\"       Compressed git output
1593    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1594    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1595    lean-ctx gain                  Visual terminal dashboard
1596    lean-ctx gain --live           Live auto-updating terminal dashboard
1597    lean-ctx gain --graph          30-day savings chart
1598    lean-ctx gain --daily          Day-by-day breakdown with USD
1599         lean-ctx token-report --json   Machine-readable token + memory report
1600    lean-ctx dashboard             Open web dashboard at localhost:3333
1601    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1602    lean-ctx gain --wrapped        Wrapped report card (recommended)
1603    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1604    lean-ctx sessions list         List all CCP sessions
1605    lean-ctx sessions show         Show latest session state
1606    lean-ctx discover              Find missed savings in shell history
1607    lean-ctx setup                 One-command setup (shell + editors + verify)
1608    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1609    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1610    lean-ctx bootstrap --json      Machine-readable bootstrap report
1611    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1612    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1613    lean-ctx-off                   Disable all shell aliases
1614    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1615    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1616    lean-ctx-mode off              Same as lean-ctx-off
1617    lean-ctx-status                Show whether compression is active
1618    lean-ctx init --agent pi       Install Pi Coding Agent extension
1619    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1620    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1621    lean-ctx doctor --fix --json   Repair + machine-readable report
1622    lean-ctx status --json         Machine-readable current status
1623    lean-ctx session task \"implement auth\"
1624    lean-ctx session finding \"auth.rs:42 — missing validation\"
1625    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1626    lean-ctx knowledge recall \"authentication\"
1627    lean-ctx knowledge search \"database migration\"
1628    lean-ctx overview \"refactor auth module\"
1629    lean-ctx compress --signatures
1630    lean-ctx read src/main.rs -m map
1631    lean-ctx grep \"pub fn\" src/
1632    lean-ctx deps .
1633
1634CLOUD:
1635    cloud status                   Show cloud connection status
1636    login <email>                  Log into existing LeanCTX Cloud account
1637    register <email>               Create a new LeanCTX Cloud account
1638    forgot-password <email>        Send password reset email
1639    sync                           Upload local stats to cloud dashboard
1640    contribute                     Share anonymized compression data
1641
1642TROUBLESHOOTING:
1643    Commands broken?     lean-ctx-off             (fixes current session)
1644    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1645    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1646    Binary missing?      Aliases auto-fallback to original commands (safe)
1647    Preview init?        lean-ctx init --global --dry-run
1648
1649WEBSITE: https://leanctx.com
1650GITHUB:  https://github.com/yvgude/lean-ctx
1651",
1652        version = env!("CARGO_PKG_VERSION"),
1653    );
1654}
1655
1656fn cmd_stop() {
1657    use crate::daemon;
1658    use crate::ipc;
1659
1660    eprintln!("Stopping all lean-ctx processes…");
1661
1662    // 1. Unload LaunchAgent/systemd first to prevent respawning
1663    crate::proxy_autostart::stop();
1664    eprintln!("  Unloaded autostart (LaunchAgent/systemd).");
1665
1666    // 2. Stop daemon via IPC
1667    if let Err(e) = daemon::stop_daemon() {
1668        eprintln!("  Warning: daemon stop: {e}");
1669    }
1670
1671    // 3. SIGTERM all remaining lean-ctx processes
1672    let killed = ipc::process::kill_all_by_name("lean-ctx");
1673    if killed > 0 {
1674        eprintln!("  Sent SIGTERM to {killed} process(es).");
1675    }
1676
1677    std::thread::sleep(std::time::Duration::from_millis(500));
1678
1679    // 4. Force-kill stragglers (but never MCP servers — IDE will respawn them)
1680    let remaining = ipc::process::find_killable_pids("lean-ctx");
1681    if !remaining.is_empty() {
1682        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1683        for &pid in &remaining {
1684            let _ = ipc::process::force_kill(pid);
1685        }
1686        std::thread::sleep(std::time::Duration::from_millis(300));
1687    }
1688
1689    daemon::cleanup_daemon_files();
1690
1691    let final_check = ipc::process::find_killable_pids("lean-ctx");
1692    if final_check.is_empty() {
1693        eprintln!("  ✓ All lean-ctx processes stopped.");
1694    } else {
1695        eprintln!(
1696            "  ✗ {} process(es) could not be killed: {:?}",
1697            final_check.len(),
1698            final_check
1699        );
1700        eprintln!(
1701            "    Try: sudo kill -9 {}",
1702            final_check
1703                .iter()
1704                .map(std::string::ToString::to_string)
1705                .collect::<Vec<_>>()
1706                .join(" ")
1707        );
1708        std::process::exit(1);
1709    }
1710}
1711
1712fn cmd_restart() {
1713    use crate::daemon;
1714    use crate::ipc;
1715
1716    eprintln!("Restarting lean-ctx…");
1717
1718    // Stop autostart first to prevent respawning during restart
1719    crate::proxy_autostart::stop();
1720
1721    if let Err(e) = daemon::stop_daemon() {
1722        eprintln!("  Warning: daemon stop: {e}");
1723    }
1724
1725    let orphans = ipc::process::kill_all_by_name("lean-ctx");
1726    if orphans > 0 {
1727        eprintln!("  Terminated {orphans} orphan process(es).");
1728    }
1729
1730    std::thread::sleep(std::time::Duration::from_millis(500));
1731
1732    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1733    if !remaining.is_empty() {
1734        eprintln!(
1735            "  Force-killing {} stubborn process(es): {:?}",
1736            remaining.len(),
1737            remaining
1738        );
1739        for &pid in &remaining {
1740            let _ = ipc::process::force_kill(pid);
1741        }
1742        std::thread::sleep(std::time::Duration::from_millis(300));
1743    }
1744
1745    daemon::cleanup_daemon_files();
1746
1747    // Re-enable autostart
1748    crate::proxy_autostart::start();
1749
1750    match daemon::start_daemon(&[]) {
1751        Ok(()) => eprintln!("  ✓ Daemon restarted. Config changes are now active."),
1752        Err(e) => {
1753            eprintln!("  ✗ Daemon start failed: {e}");
1754            std::process::exit(1);
1755        }
1756    }
1757}
1758
1759fn cmd_dev_install() {
1760    use crate::ipc;
1761
1762    let cargo_root = find_cargo_project_root();
1763    let Some(cargo_root) = cargo_root else {
1764        eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1765        std::process::exit(1);
1766    };
1767
1768    eprintln!("Building release binary…");
1769    let build = std::process::Command::new("cargo")
1770        .args(["build", "--release"])
1771        .current_dir(&cargo_root)
1772        .status();
1773
1774    match build {
1775        Ok(s) if s.success() => {}
1776        Ok(s) => {
1777            eprintln!("  Build failed with exit code {}", s.code().unwrap_or(-1));
1778            std::process::exit(1);
1779        }
1780        Err(e) => {
1781            eprintln!("  Build failed: {e}");
1782            std::process::exit(1);
1783        }
1784    }
1785
1786    let built_binary = cargo_root.join("target/release/lean-ctx");
1787    if !built_binary.exists() {
1788        eprintln!(
1789            "  Error: Built binary not found at {}",
1790            built_binary.display()
1791        );
1792        std::process::exit(1);
1793    }
1794
1795    let install_path = resolve_install_path();
1796    eprintln!("Installing to {}…", install_path.display());
1797
1798    eprintln!("  Stopping all lean-ctx processes…");
1799    crate::proxy_autostart::stop();
1800    let _ = crate::daemon::stop_daemon();
1801    ipc::process::kill_all_by_name("lean-ctx");
1802    std::thread::sleep(std::time::Duration::from_millis(500));
1803
1804    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1805    if !remaining.is_empty() {
1806        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1807        for &pid in &remaining {
1808            let _ = ipc::process::force_kill(pid);
1809        }
1810        std::thread::sleep(std::time::Duration::from_millis(500));
1811    }
1812
1813    let old_path = install_path.with_extension("old");
1814    if install_path.exists() {
1815        if let Err(e) = std::fs::rename(&install_path, &old_path) {
1816            eprintln!("  Warning: rename existing binary: {e}");
1817        }
1818    }
1819
1820    match std::fs::copy(&built_binary, &install_path) {
1821        Ok(_) => {
1822            let _ = std::fs::remove_file(&old_path);
1823            #[cfg(unix)]
1824            {
1825                use std::os::unix::fs::PermissionsExt;
1826                let _ =
1827                    std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1828            }
1829            eprintln!("  ✓ Binary installed.");
1830        }
1831        Err(e) => {
1832            eprintln!("  Error: copy failed: {e}");
1833            if old_path.exists() {
1834                let _ = std::fs::rename(&old_path, &install_path);
1835                eprintln!("  Rolled back to previous binary.");
1836            }
1837            std::process::exit(1);
1838        }
1839    }
1840
1841    let version = std::process::Command::new(&install_path)
1842        .arg("--version")
1843        .output()
1844        .map_or_else(
1845            |_| "unknown".to_string(),
1846            |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1847        );
1848
1849    eprintln!("  ✓ dev-install complete: {version}");
1850
1851    eprintln!("  Re-enabling autostart…");
1852    crate::proxy_autostart::start();
1853
1854    eprintln!("  Starting daemon…");
1855    match crate::daemon::start_daemon(&[]) {
1856        Ok(()) => {}
1857        Err(e) => eprintln!("  Warning: daemon start: {e} (will be started by editor)"),
1858    }
1859}
1860
1861fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1862    let mut dir = std::env::current_dir().ok()?;
1863    loop {
1864        if dir.join("Cargo.toml").exists() {
1865            return Some(dir);
1866        }
1867        if !dir.pop() {
1868            return None;
1869        }
1870    }
1871}
1872
1873fn resolve_install_path() -> std::path::PathBuf {
1874    if let Ok(exe) = std::env::current_exe() {
1875        if let Ok(canonical) = exe.canonicalize() {
1876            let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1877            if !is_in_cargo_target && canonical.exists() {
1878                return canonical;
1879            }
1880        }
1881    }
1882
1883    if let Ok(home) = std::env::var("HOME") {
1884        let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1885        if local_bin.parent().is_some_and(std::path::Path::exists) {
1886            return local_bin;
1887        }
1888    }
1889
1890    std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1891}
1892
1893fn spawn_proxy_if_needed() {
1894    use std::net::TcpStream;
1895    use std::time::Duration;
1896
1897    let port = crate::proxy_setup::default_port();
1898    let already_running = TcpStream::connect_timeout(
1899        &format!("127.0.0.1:{port}").parse().unwrap(),
1900        Duration::from_millis(200),
1901    )
1902    .is_ok();
1903
1904    if already_running {
1905        tracing::debug!("proxy already running on port {port}");
1906        return;
1907    }
1908
1909    let binary = std::env::current_exe().map_or_else(
1910        |_| "lean-ctx".to_string(),
1911        |p| p.to_string_lossy().to_string(),
1912    );
1913
1914    match std::process::Command::new(&binary)
1915        .args(["proxy", "start", &format!("--port={port}")])
1916        .stdin(std::process::Stdio::null())
1917        .stdout(std::process::Stdio::null())
1918        .stderr(std::process::Stdio::null())
1919        .spawn()
1920    {
1921        Ok(_) => tracing::info!("auto-started proxy on port {port}"),
1922        Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
1923    }
1924}
1925
1926fn resolve_worker_threads(parallelism: usize) -> usize {
1927    std::env::var("LEAN_CTX_WORKER_THREADS")
1928        .ok()
1929        .and_then(|v| v.parse::<usize>().ok())
1930        .unwrap_or_else(|| parallelism.clamp(1, 4))
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935    use super::*;
1936
1937    #[test]
1938    fn worker_threads_default_clamps_low() {
1939        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1940        assert_eq!(resolve_worker_threads(1), 1);
1941    }
1942
1943    #[test]
1944    fn worker_threads_default_clamps_high() {
1945        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1946        assert_eq!(resolve_worker_threads(32), 4);
1947    }
1948
1949    #[test]
1950    fn worker_threads_default_passthrough() {
1951        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1952        assert_eq!(resolve_worker_threads(3), 3);
1953    }
1954
1955    #[test]
1956    fn worker_threads_env_override() {
1957        std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
1958        assert_eq!(resolve_worker_threads(2), 12);
1959        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1960    }
1961
1962    #[test]
1963    fn worker_threads_env_invalid_falls_back() {
1964        std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
1965        assert_eq!(resolve_worker_threads(3), 3);
1966        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1967    }
1968}