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