Skip to main content

lean_ctx/cli/
dispatch.rs

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