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                            if let Ok(resp) =
830                                ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
831                            {
832                                let body = resp.into_body().read_to_string().unwrap_or_default();
833                                if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
834                                    println!("lean-ctx proxy status:");
835                                    println!("  Requests:    {}", v["requests_total"]);
836                                    println!("  Compressed:  {}", v["requests_compressed"]);
837                                    println!("  Tokens saved: {}", v["tokens_saved"]);
838                                    println!(
839                                        "  Compression: {}%",
840                                        v["compression_ratio_pct"].as_str().unwrap_or("0.0")
841                                    );
842                                } else {
843                                    println!("{body}");
844                                }
845                            } else {
846                                println!("No proxy running on port {port}.");
847                                println!("Start with: lean-ctx proxy start");
848                            }
849                        }
850                        _ => {
851                            println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
852                        }
853                    }
854                    return;
855                }
856                #[cfg(not(feature = "http-server"))]
857                {
858                    eprintln!("lean-ctx proxy is not available in this build");
859                    std::process::exit(1);
860                }
861            }
862            "init" => {
863                super::cmd_init(&rest);
864                return;
865            }
866            "setup" => {
867                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
868                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
869                let fix = rest.iter().any(|a| a == "--fix");
870                let json = rest.iter().any(|a| a == "--json");
871                let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
872
873                if non_interactive || fix || json || yes {
874                    let opts = setup::SetupOptions {
875                        non_interactive,
876                        yes,
877                        fix,
878                        json,
879                        no_auto_approve,
880                    };
881                    match setup::run_setup_with_options(opts) {
882                        Ok(report) => {
883                            if json {
884                                println!(
885                                    "{}",
886                                    serde_json::to_string_pretty(&report)
887                                        .unwrap_or_else(|_| "{}".to_string())
888                                );
889                            }
890                            if !report.success {
891                                std::process::exit(1);
892                            }
893                        }
894                        Err(e) => {
895                            eprintln!("{e}");
896                            std::process::exit(1);
897                        }
898                    }
899                } else {
900                    setup::run_setup();
901                }
902                return;
903            }
904            "install" => {
905                let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
906                let json = rest.iter().any(|a| a == "--json");
907                if !repair {
908                    eprintln!("Usage: lean-ctx install --repair [--json]");
909                    std::process::exit(1);
910                }
911                let opts = setup::SetupOptions {
912                    non_interactive: true,
913                    yes: true,
914                    fix: true,
915                    json,
916                    ..Default::default()
917                };
918                match setup::run_setup_with_options(opts) {
919                    Ok(report) => {
920                        if json {
921                            println!(
922                                "{}",
923                                serde_json::to_string_pretty(&report)
924                                    .unwrap_or_else(|_| "{}".to_string())
925                            );
926                        }
927                        if !report.success {
928                            std::process::exit(1);
929                        }
930                    }
931                    Err(e) => {
932                        eprintln!("{e}");
933                        std::process::exit(1);
934                    }
935                }
936                return;
937            }
938            "bootstrap" => {
939                let json = rest.iter().any(|a| a == "--json");
940                let opts = setup::SetupOptions {
941                    non_interactive: true,
942                    yes: true,
943                    fix: true,
944                    json,
945                    ..Default::default()
946                };
947                match setup::run_setup_with_options(opts) {
948                    Ok(report) => {
949                        if json {
950                            println!(
951                                "{}",
952                                serde_json::to_string_pretty(&report)
953                                    .unwrap_or_else(|_| "{}".to_string())
954                            );
955                        }
956                        if !report.success {
957                            std::process::exit(1);
958                        }
959                    }
960                    Err(e) => {
961                        eprintln!("{e}");
962                        std::process::exit(1);
963                    }
964                }
965                return;
966            }
967            "status" => {
968                let code = status::run_cli(&rest);
969                if code != 0 {
970                    std::process::exit(code);
971                }
972                return;
973            }
974            "read" => {
975                super::cmd_read(&rest);
976                core::stats::flush();
977                return;
978            }
979            "diff" => {
980                super::cmd_diff(&rest);
981                core::stats::flush();
982                return;
983            }
984            "grep" => {
985                super::cmd_grep(&rest);
986                core::stats::flush();
987                return;
988            }
989            "find" => {
990                super::cmd_find(&rest);
991                core::stats::flush();
992                return;
993            }
994            "ls" => {
995                super::cmd_ls(&rest);
996                core::stats::flush();
997                return;
998            }
999            "deps" => {
1000                super::cmd_deps(&rest);
1001                core::stats::flush();
1002                return;
1003            }
1004            "discover" => {
1005                super::cmd_discover(&rest);
1006                return;
1007            }
1008            "ghost" => {
1009                super::cmd_ghost(&rest);
1010                return;
1011            }
1012            "filter" => {
1013                super::cmd_filter(&rest);
1014                return;
1015            }
1016            "heatmap" => {
1017                heatmap::cmd_heatmap(&rest);
1018                return;
1019            }
1020            "graph" => {
1021                let sub = rest.first().map_or("build", std::string::String::as_str);
1022                match sub {
1023                    "build" => {
1024                        let root = rest.get(1).cloned().or_else(|| {
1025                            std::env::current_dir()
1026                                .ok()
1027                                .map(|p| p.to_string_lossy().to_string())
1028                        });
1029                        let root = root.unwrap_or_else(|| ".".to_string());
1030                        let index = core::graph_index::load_or_build(&root);
1031                        println!(
1032                            "Graph built: {} files, {} edges",
1033                            index.files.len(),
1034                            index.edges.len()
1035                        );
1036                    }
1037                    "export-html" => {
1038                        let mut root: Option<String> = None;
1039                        let mut out: Option<String> = None;
1040                        let mut max_nodes: usize = 2500;
1041
1042                        let args = &rest[1..];
1043                        let mut i = 0usize;
1044                        while i < args.len() {
1045                            let a = args[i].as_str();
1046                            if let Some(v) = a.strip_prefix("--root=") {
1047                                root = Some(v.to_string());
1048                            } else if a == "--root" {
1049                                root = args.get(i + 1).cloned();
1050                                i += 1;
1051                            } else if let Some(v) = a.strip_prefix("--out=") {
1052                                out = Some(v.to_string());
1053                            } else if a == "--out" {
1054                                out = args.get(i + 1).cloned();
1055                                i += 1;
1056                            } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1057                                max_nodes = v.parse::<usize>().unwrap_or(0);
1058                            } else if a == "--max-nodes" {
1059                                let v = args.get(i + 1).map_or("", String::as_str);
1060                                max_nodes = v.parse::<usize>().unwrap_or(0);
1061                                i += 1;
1062                            }
1063                            i += 1;
1064                        }
1065
1066                        let root = root
1067                            .or_else(|| {
1068                                std::env::current_dir()
1069                                    .ok()
1070                                    .map(|p| p.to_string_lossy().to_string())
1071                            })
1072                            .unwrap_or_else(|| ".".to_string());
1073                        let Some(out) = out else {
1074                            eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1075                            std::process::exit(1);
1076                        };
1077                        if max_nodes == 0 {
1078                            eprintln!("--max-nodes must be >= 1");
1079                            std::process::exit(1);
1080                        }
1081
1082                        core::graph_export::export_graph_html(
1083                            &root,
1084                            std::path::Path::new(&out),
1085                            max_nodes,
1086                        )
1087                        .unwrap_or_else(|e| {
1088                            eprintln!("graph export failed: {e}");
1089                            std::process::exit(1);
1090                        });
1091                        println!("{out}");
1092                    }
1093                    _ => {
1094                        eprintln!(
1095                            "Usage:\n  lean-ctx graph build [path]\n  lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1096                        );
1097                        std::process::exit(1);
1098                    }
1099                }
1100                return;
1101            }
1102            "smells" => {
1103                let action = rest.first().map_or("summary", String::as_str);
1104                let rule = rest.iter().enumerate().find_map(|(i, a)| {
1105                    if let Some(v) = a.strip_prefix("--rule=") {
1106                        return Some(v.to_string());
1107                    }
1108                    if a == "--rule" {
1109                        return rest.get(i + 1).cloned();
1110                    }
1111                    None
1112                });
1113                let path = rest.iter().enumerate().find_map(|(i, a)| {
1114                    if let Some(v) = a.strip_prefix("--path=") {
1115                        return Some(v.to_string());
1116                    }
1117                    if a == "--path" {
1118                        return rest.get(i + 1).cloned();
1119                    }
1120                    None
1121                });
1122                let root = rest
1123                    .iter()
1124                    .enumerate()
1125                    .find_map(|(i, a)| {
1126                        if let Some(v) = a.strip_prefix("--root=") {
1127                            return Some(v.to_string());
1128                        }
1129                        if a == "--root" {
1130                            return rest.get(i + 1).cloned();
1131                        }
1132                        None
1133                    })
1134                    .or_else(|| {
1135                        std::env::current_dir()
1136                            .ok()
1137                            .map(|p| p.to_string_lossy().to_string())
1138                    })
1139                    .unwrap_or_else(|| ".".to_string());
1140                let fmt = if rest.iter().any(|a| a == "--json") {
1141                    Some("json")
1142                } else {
1143                    None
1144                };
1145                println!(
1146                    "{}",
1147                    tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1148                );
1149                return;
1150            }
1151            "session" => {
1152                super::cmd_session_action(&rest);
1153                return;
1154            }
1155            "control" | "context-control" => {
1156                super::cmd_control(&rest);
1157                return;
1158            }
1159            "plan" | "context-plan" => {
1160                super::cmd_plan(&rest);
1161                return;
1162            }
1163            "compile" | "context-compile" => {
1164                super::cmd_compile(&rest);
1165                return;
1166            }
1167            "knowledge" => {
1168                super::cmd_knowledge(&rest);
1169                return;
1170            }
1171            "overview" => {
1172                super::cmd_overview(&rest);
1173                return;
1174            }
1175            "compress" => {
1176                super::cmd_compress(&rest);
1177                return;
1178            }
1179            "wrapped" => {
1180                super::cmd_wrapped(&rest);
1181                return;
1182            }
1183            "sessions" => {
1184                super::cmd_sessions(&rest);
1185                return;
1186            }
1187            "benchmark" => {
1188                super::cmd_benchmark(&rest);
1189                return;
1190            }
1191            "profile" => {
1192                super::cmd_profile(&rest);
1193                return;
1194            }
1195            "config" => {
1196                super::cmd_config(&rest);
1197                return;
1198            }
1199            "stats" => {
1200                super::cmd_stats(&rest);
1201                return;
1202            }
1203            "cache" => {
1204                super::cmd_cache(&rest);
1205                return;
1206            }
1207            "theme" => {
1208                super::cmd_theme(&rest);
1209                return;
1210            }
1211            "tee" => {
1212                super::cmd_tee(&rest);
1213                return;
1214            }
1215            "terse" | "compression" => {
1216                super::cmd_compression(&rest);
1217                return;
1218            }
1219            "slow-log" => {
1220                super::cmd_slow_log(&rest);
1221                return;
1222            }
1223            "update" | "--self-update" => {
1224                core::updater::run(&rest);
1225                return;
1226            }
1227            "restart" => {
1228                cmd_restart();
1229                return;
1230            }
1231            "stop" => {
1232                cmd_stop();
1233                return;
1234            }
1235            "dev-install" => {
1236                cmd_dev_install();
1237                return;
1238            }
1239            "doctor" => {
1240                let code = doctor::run_cli(&rest);
1241                if code != 0 {
1242                    std::process::exit(code);
1243                }
1244                return;
1245            }
1246            "gotchas" | "bugs" => {
1247                super::cloud::cmd_gotchas(&rest);
1248                return;
1249            }
1250            "learn" => {
1251                super::cmd_learn(&rest);
1252                return;
1253            }
1254            "buddy" | "pet" => {
1255                super::cloud::cmd_buddy(&rest);
1256                return;
1257            }
1258            "hook" => {
1259                hook_handlers::mark_hook_environment();
1260                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1261                let action = rest.first().map_or("help", std::string::String::as_str);
1262                match action {
1263                    "rewrite" => hook_handlers::handle_rewrite(),
1264                    "redirect" => hook_handlers::handle_redirect(),
1265                    "observe" => hook_handlers::handle_observe(),
1266                    "copilot" => hook_handlers::handle_copilot(),
1267                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1268                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1269                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1270                    _ => {
1271                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1272                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1273                        std::process::exit(1);
1274                    }
1275                }
1276                return;
1277            }
1278            "report-issue" | "report" => {
1279                report::run(&rest);
1280                return;
1281            }
1282            "uninstall" => {
1283                let dry_run = rest.iter().any(|a| a == "--dry-run");
1284                uninstall::run(dry_run);
1285                return;
1286            }
1287            "bypass" => {
1288                if rest.is_empty() {
1289                    eprintln!("Usage: lean-ctx bypass \"command\"");
1290                    eprintln!("Runs the command with zero compression (raw passthrough).");
1291                    std::process::exit(1);
1292                }
1293                let command = if rest.len() == 1 {
1294                    rest[0].clone()
1295                } else {
1296                    shell::join_command(&args[2..])
1297                };
1298                std::env::set_var("LEAN_CTX_RAW", "1");
1299                let code = shell::exec(&command);
1300                std::process::exit(code);
1301            }
1302            "safety-levels" | "safety" => {
1303                println!("{}", core::compression_safety::format_safety_table());
1304                return;
1305            }
1306            "cheat" | "cheatsheet" | "cheat-sheet" => {
1307                super::cmd_cheatsheet();
1308                return;
1309            }
1310            "login" => {
1311                super::cloud::cmd_login(&rest);
1312                return;
1313            }
1314            "register" => {
1315                super::cloud::cmd_register(&rest);
1316                return;
1317            }
1318            "forgot-password" => {
1319                super::cloud::cmd_forgot_password(&rest);
1320                return;
1321            }
1322            "sync" => {
1323                super::cloud::cmd_sync();
1324                return;
1325            }
1326            "contribute" => {
1327                super::cloud::cmd_contribute();
1328                return;
1329            }
1330            "cloud" => {
1331                super::cloud::cmd_cloud(&rest);
1332                return;
1333            }
1334            "upgrade" => {
1335                super::cloud::cmd_upgrade();
1336                return;
1337            }
1338            "--version" | "-V" => {
1339                println!("{}", core::integrity::origin_line());
1340                return;
1341            }
1342            "--help" | "-h" => {
1343                print_help();
1344                return;
1345            }
1346            "mcp" => {}
1347            _ => {
1348                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1349                print_help();
1350                std::process::exit(1);
1351            }
1352        }
1353    }
1354
1355    if let Err(e) = run_mcp_server() {
1356        tracing::error!("lean-ctx: {e}");
1357        std::process::exit(1);
1358    }
1359}
1360
1361fn passthrough(command: &str) -> ! {
1362    let (shell, flag) = shell::shell_and_flag();
1363    let status = std::process::Command::new(&shell)
1364        .arg(&flag)
1365        .arg(command)
1366        .env("LEAN_CTX_ACTIVE", "1")
1367        .status()
1368        .map_or(127, |s| s.code().unwrap_or(1));
1369    std::process::exit(status);
1370}
1371
1372fn run_async<F: std::future::Future>(future: F) -> F::Output {
1373    tokio::runtime::Runtime::new()
1374        .expect("failed to create async runtime")
1375        .block_on(future)
1376}
1377
1378fn run_mcp_server() -> Result<()> {
1379    use rmcp::ServiceExt;
1380
1381    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1382
1383    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1384
1385    // Concurrency hardening:
1386    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1387    // - Limits Tokio worker/blocking threads to avoid host degradation.
1388    // - LEAN_CTX_WORKER_THREADS overrides the default for environments
1389    //   with many concurrent subagents (e.g. parallel review pipelines).
1390    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1391        "mcp-startup",
1392        std::time::Duration::from_secs(3),
1393        std::time::Duration::from_secs(30),
1394    );
1395
1396    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1397    let worker_threads = resolve_worker_threads(parallelism);
1398    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1399
1400    let rt = tokio::runtime::Builder::new_multi_thread()
1401        .worker_threads(worker_threads)
1402        .max_blocking_threads(max_blocking_threads)
1403        .enable_all()
1404        .build()?;
1405
1406    let server = tools::create_server();
1407    drop(startup_lock);
1408
1409    // Auto-start proxy in background so the dashboard gets exact token data.
1410    spawn_proxy_if_needed();
1411
1412    rt.block_on(async {
1413        core::logging::init_mcp_logging();
1414        core::protocol::set_mcp_context(true);
1415
1416        tracing::info!(
1417            "lean-ctx v{} MCP server starting",
1418            env!("CARGO_PKG_VERSION")
1419        );
1420
1421        let transport =
1422            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1423        let server_handle = server.clone();
1424        let service = match server.serve(transport).await {
1425            Ok(s) => s,
1426            Err(e) => {
1427                let msg = e.to_string();
1428                if msg.contains("expect initialized")
1429                    || msg.contains("context canceled")
1430                    || msg.contains("broken pipe")
1431                {
1432                    tracing::debug!("Client disconnected before init: {msg}");
1433                    return Ok(());
1434                }
1435                return Err(e.into());
1436            }
1437        };
1438        match service.waiting().await {
1439            Ok(reason) => {
1440                tracing::info!("MCP server stopped: {reason:?}");
1441            }
1442            Err(e) => {
1443                let msg = e.to_string();
1444                if msg.contains("broken pipe")
1445                    || msg.contains("connection reset")
1446                    || msg.contains("context canceled")
1447                {
1448                    tracing::info!("MCP server: transport closed ({msg})");
1449                } else {
1450                    tracing::error!("MCP server error: {msg}");
1451                }
1452            }
1453        }
1454
1455        server_handle.shutdown().await;
1456
1457        core::stats::flush();
1458        core::heatmap::flush();
1459        core::mode_predictor::ModePredictor::flush();
1460        core::feedback::FeedbackStore::flush();
1461
1462        Ok(())
1463    })
1464}
1465
1466fn print_help() {
1467    println!(
1468        "lean-ctx {version} — Context Runtime for AI Agents
1469
147060+ compression patterns | 51 MCP tools | 10 read modes | Context Continuity Protocol
1471
1472USAGE:
1473    lean-ctx                       Start MCP server (stdio)
1474    lean-ctx serve                 Start MCP server (Streamable HTTP)
1475    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1476    lean-ctx serve --stop          Stop running daemon
1477    lean-ctx serve --status        Show daemon status
1478    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1479    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1480    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1481    lean-ctx exec \"command\"        Same as -c
1482    lean-ctx shell                 Interactive shell with compression
1483
1484COMMANDS:
1485    gain                           Visual dashboard (colors, bars, sparklines, USD)
1486    gain --live                    Live mode: auto-refreshes every 1s in-place
1487    gain --graph                   30-day savings chart
1488    gain --daily                   Bordered day-by-day table with USD
1489    gain --json                    Raw JSON export of all stats
1490         token-report [--json]          Token + memory report (project + session + CEP)
1491    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1492    index <status|build|build-full|watch>  Codebase index utilities
1493    cep                            CEP impact report (score trends, cache, modes)
1494    watch                          Live TUI dashboard (real-time event stream)
1495    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1496    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1497    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1498    proxy status                   Show proxy statistics
1499    cache [list|clear|stats]       Show/manage file read cache
1500    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1501    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1502    benchmark run [path] [--json]  Run real benchmark on project files
1503    benchmark report [path]        Generate shareable Markdown report
1504    cheatsheet                     Command cheat sheet & workflow quick reference
1505    setup                          One-command setup: shell + editor + verify
1506    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1507    bootstrap                      Non-interactive setup + fix (zero-config)
1508    status [--json]                Show setup + MCP + rules status
1509    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1510    init --agent <name>            Configure MCP for specific editor/agent
1511    read <file> [-m mode]          Read file with compression
1512    diff <file1> <file2>           Compressed file diff
1513    grep <pattern> [path]          Search with compressed output
1514    find <pattern> [path]          Find files with compressed output
1515    ls [path]                      Directory listing with compression
1516    deps [path]                    Show project dependencies
1517    discover                       Find uncompressed commands in shell history
1518    ghost [--json]                 Ghost Token report: find hidden token waste
1519    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1520    session                        Show adoption statistics
1521    session task <desc>            Set current task
1522    session finding <summary>      Record a finding
1523    session save                   Save current session
1524    session load [id]              Load session (latest if no ID)
1525    knowledge remember <value> --category <c> --key <k>   Store a fact
1526    knowledge recall [query] [--category <c>]             Retrieve facts
1527    knowledge search <query>       Cross-project knowledge search
1528    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1529    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1530    knowledge remove --category <c> --key <k>             Remove a fact
1531    knowledge status               Knowledge base summary
1532    overview [task]                Project overview (task-contextualized if given)
1533    compress [--signatures]        Context compression checkpoint
1534    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1535    profile [list|show|diff|create|set]  Manage context profiles
1536    theme [list|set|export|import] Customize terminal colors and themes
1537    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1538    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1539    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1540    update [--check]               Self-update lean-ctx binary from GitHub Releases
1541    stop                           Stop ALL lean-ctx processes (daemon, proxy, orphans)
1542    restart                        Restart daemon (applies config.toml changes)
1543    dev-install                    Build release + atomic install + restart (for development)
1544    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1545    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1546    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1547    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1548    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1549                                   Code smell detection (Property Graph, 8 rules)
1550    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1551    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1552    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1553    uninstall                      Remove shell hook, MCP configs, and data directory
1554
1555SHELL HOOK PATTERNS (95+):
1556    git       status, log, diff, add, commit, push, pull, fetch, clone,
1557              branch, checkout, switch, merge, stash, tag, reset, remote
1558    docker    build, ps, images, logs, compose, exec, network
1559    npm/pnpm  install, test, run, list, outdated, audit
1560    cargo     build, test, check, clippy
1561    gh        pr list/view/create, issue list/view, run list/view
1562    kubectl   get pods/services/deployments, logs, describe, apply
1563    python    pip install/list/outdated, ruff check/format, poetry, uv
1564    linters   eslint, biome, prettier, golangci-lint
1565    builds    tsc, next build, vite build
1566    ruby      rubocop, bundle install/update, rake test, rails test
1567    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1568    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1569    utils     curl, grep/rg, find, ls, wget, env
1570    data      JSON schema extraction, log deduplication
1571
1572READ MODES:
1573    auto                           Auto-select optimal mode (default)
1574    full                           Full content (cached re-reads = 13 tokens)
1575    map                            Dependency graph + API signatures
1576    signatures                     tree-sitter AST extraction (18 languages)
1577    task                           Task-relevant filtering (requires ctx_session task)
1578    reference                      One-line reference stub (cheap cache key)
1579    aggressive                     Syntax-stripped content
1580    entropy                        Shannon entropy filtered
1581    diff                           Changed lines only
1582    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1583
1584ENVIRONMENT:
1585    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1586    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1587    LEAN_CTX_RAW=1                 Same as --raw for current command
1588    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1589    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1590
1591OPTIONS:
1592    --version, -V                  Show version
1593    --help, -h                     Show this help
1594
1595EXAMPLES:
1596    lean-ctx -c \"git status\"       Compressed git output
1597    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1598    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1599    lean-ctx gain                  Visual terminal dashboard
1600    lean-ctx gain --live           Live auto-updating terminal dashboard
1601    lean-ctx gain --graph          30-day savings chart
1602    lean-ctx gain --daily          Day-by-day breakdown with USD
1603         lean-ctx token-report --json   Machine-readable token + memory report
1604    lean-ctx dashboard             Open web dashboard at localhost:3333
1605    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1606    lean-ctx gain --wrapped        Wrapped report card (recommended)
1607    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1608    lean-ctx sessions list         List all CCP sessions
1609    lean-ctx sessions show         Show latest session state
1610    lean-ctx discover              Find missed savings in shell history
1611    lean-ctx setup                 One-command setup (shell + editors + verify)
1612    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1613    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1614    lean-ctx bootstrap --json      Machine-readable bootstrap report
1615    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1616    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1617    lean-ctx-off                   Disable all shell aliases
1618    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1619    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1620    lean-ctx-mode off              Same as lean-ctx-off
1621    lean-ctx-status                Show whether compression is active
1622    lean-ctx init --agent pi       Install Pi Coding Agent extension
1623    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1624    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1625    lean-ctx doctor --fix --json   Repair + machine-readable report
1626    lean-ctx status --json         Machine-readable current status
1627    lean-ctx session task \"implement auth\"
1628    lean-ctx session finding \"auth.rs:42 — missing validation\"
1629    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1630    lean-ctx knowledge recall \"authentication\"
1631    lean-ctx knowledge search \"database migration\"
1632    lean-ctx overview \"refactor auth module\"
1633    lean-ctx compress --signatures
1634    lean-ctx read src/main.rs -m map
1635    lean-ctx grep \"pub fn\" src/
1636    lean-ctx deps .
1637
1638CLOUD:
1639    cloud status                   Show cloud connection status
1640    login <email>                  Log into existing LeanCTX Cloud account
1641    register <email>               Create a new LeanCTX Cloud account
1642    forgot-password <email>        Send password reset email
1643    sync                           Upload local stats to cloud dashboard
1644    contribute                     Share anonymized compression data
1645
1646TROUBLESHOOTING:
1647    Commands broken?     lean-ctx-off             (fixes current session)
1648    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1649    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1650    Binary missing?      Aliases auto-fallback to original commands (safe)
1651    Preview init?        lean-ctx init --global --dry-run
1652
1653WEBSITE: https://leanctx.com
1654GITHUB:  https://github.com/yvgude/lean-ctx
1655",
1656        version = env!("CARGO_PKG_VERSION"),
1657    );
1658}
1659
1660fn cmd_stop() {
1661    use crate::daemon;
1662    use crate::ipc;
1663
1664    eprintln!("Stopping all lean-ctx processes…");
1665
1666    // 1. Unload LaunchAgent/systemd first to prevent respawning
1667    crate::proxy_autostart::stop();
1668    eprintln!("  Unloaded autostart (LaunchAgent/systemd).");
1669
1670    // 2. Stop daemon via IPC
1671    if let Err(e) = daemon::stop_daemon() {
1672        eprintln!("  Warning: daemon stop: {e}");
1673    }
1674
1675    // 3. SIGTERM all remaining lean-ctx processes
1676    let killed = ipc::process::kill_all_by_name("lean-ctx");
1677    if killed > 0 {
1678        eprintln!("  Sent SIGTERM to {killed} process(es).");
1679    }
1680
1681    std::thread::sleep(std::time::Duration::from_millis(500));
1682
1683    // 4. Force-kill stragglers (but never MCP servers — IDE will respawn them)
1684    let remaining = ipc::process::find_killable_pids("lean-ctx");
1685    if !remaining.is_empty() {
1686        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1687        for &pid in &remaining {
1688            let _ = ipc::process::force_kill(pid);
1689        }
1690        std::thread::sleep(std::time::Duration::from_millis(300));
1691    }
1692
1693    daemon::cleanup_daemon_files();
1694
1695    let final_check = ipc::process::find_killable_pids("lean-ctx");
1696    if final_check.is_empty() {
1697        eprintln!("  ✓ All lean-ctx processes stopped.");
1698    } else {
1699        eprintln!(
1700            "  ✗ {} process(es) could not be killed: {:?}",
1701            final_check.len(),
1702            final_check
1703        );
1704        eprintln!(
1705            "    Try: sudo kill -9 {}",
1706            final_check
1707                .iter()
1708                .map(std::string::ToString::to_string)
1709                .collect::<Vec<_>>()
1710                .join(" ")
1711        );
1712        std::process::exit(1);
1713    }
1714}
1715
1716fn cmd_restart() {
1717    use crate::daemon;
1718    use crate::ipc;
1719
1720    eprintln!("Restarting lean-ctx…");
1721
1722    // Stop autostart first to prevent respawning during restart
1723    crate::proxy_autostart::stop();
1724
1725    if let Err(e) = daemon::stop_daemon() {
1726        eprintln!("  Warning: daemon stop: {e}");
1727    }
1728
1729    let orphans = ipc::process::kill_all_by_name("lean-ctx");
1730    if orphans > 0 {
1731        eprintln!("  Terminated {orphans} orphan process(es).");
1732    }
1733
1734    std::thread::sleep(std::time::Duration::from_millis(500));
1735
1736    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1737    if !remaining.is_empty() {
1738        eprintln!(
1739            "  Force-killing {} stubborn process(es): {:?}",
1740            remaining.len(),
1741            remaining
1742        );
1743        for &pid in &remaining {
1744            let _ = ipc::process::force_kill(pid);
1745        }
1746        std::thread::sleep(std::time::Duration::from_millis(300));
1747    }
1748
1749    daemon::cleanup_daemon_files();
1750
1751    // Re-enable autostart
1752    crate::proxy_autostart::start();
1753
1754    match daemon::start_daemon(&[]) {
1755        Ok(()) => eprintln!("  ✓ Daemon restarted. Config changes are now active."),
1756        Err(e) => {
1757            eprintln!("  ✗ Daemon start failed: {e}");
1758            std::process::exit(1);
1759        }
1760    }
1761}
1762
1763fn cmd_dev_install() {
1764    use crate::ipc;
1765
1766    let cargo_root = find_cargo_project_root();
1767    let Some(cargo_root) = cargo_root else {
1768        eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1769        std::process::exit(1);
1770    };
1771
1772    eprintln!("Building release binary…");
1773    let build = std::process::Command::new("cargo")
1774        .args(["build", "--release"])
1775        .current_dir(&cargo_root)
1776        .status();
1777
1778    match build {
1779        Ok(s) if s.success() => {}
1780        Ok(s) => {
1781            eprintln!("  Build failed with exit code {}", s.code().unwrap_or(-1));
1782            std::process::exit(1);
1783        }
1784        Err(e) => {
1785            eprintln!("  Build failed: {e}");
1786            std::process::exit(1);
1787        }
1788    }
1789
1790    let built_binary = cargo_root.join("target/release/lean-ctx");
1791    if !built_binary.exists() {
1792        eprintln!(
1793            "  Error: Built binary not found at {}",
1794            built_binary.display()
1795        );
1796        std::process::exit(1);
1797    }
1798
1799    let install_path = resolve_install_path();
1800    eprintln!("Installing to {}…", install_path.display());
1801
1802    eprintln!("  Stopping all lean-ctx processes…");
1803    crate::proxy_autostart::stop();
1804    let _ = crate::daemon::stop_daemon();
1805    ipc::process::kill_all_by_name("lean-ctx");
1806    std::thread::sleep(std::time::Duration::from_millis(500));
1807
1808    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1809    if !remaining.is_empty() {
1810        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1811        for &pid in &remaining {
1812            let _ = ipc::process::force_kill(pid);
1813        }
1814        std::thread::sleep(std::time::Duration::from_millis(500));
1815    }
1816
1817    let old_path = install_path.with_extension("old");
1818    if install_path.exists() {
1819        if let Err(e) = std::fs::rename(&install_path, &old_path) {
1820            eprintln!("  Warning: rename existing binary: {e}");
1821        }
1822    }
1823
1824    match std::fs::copy(&built_binary, &install_path) {
1825        Ok(_) => {
1826            let _ = std::fs::remove_file(&old_path);
1827            #[cfg(unix)]
1828            {
1829                use std::os::unix::fs::PermissionsExt;
1830                let _ =
1831                    std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1832            }
1833            eprintln!("  ✓ Binary installed.");
1834        }
1835        Err(e) => {
1836            eprintln!("  Error: copy failed: {e}");
1837            if old_path.exists() {
1838                let _ = std::fs::rename(&old_path, &install_path);
1839                eprintln!("  Rolled back to previous binary.");
1840            }
1841            std::process::exit(1);
1842        }
1843    }
1844
1845    let version = std::process::Command::new(&install_path)
1846        .arg("--version")
1847        .output()
1848        .map_or_else(
1849            |_| "unknown".to_string(),
1850            |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1851        );
1852
1853    eprintln!("  ✓ dev-install complete: {version}");
1854
1855    eprintln!("  Re-enabling autostart…");
1856    crate::proxy_autostart::start();
1857
1858    eprintln!("  Starting daemon…");
1859    match crate::daemon::start_daemon(&[]) {
1860        Ok(()) => {}
1861        Err(e) => eprintln!("  Warning: daemon start: {e} (will be started by editor)"),
1862    }
1863}
1864
1865fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1866    let mut dir = std::env::current_dir().ok()?;
1867    loop {
1868        if dir.join("Cargo.toml").exists() {
1869            return Some(dir);
1870        }
1871        if !dir.pop() {
1872            return None;
1873        }
1874    }
1875}
1876
1877fn resolve_install_path() -> std::path::PathBuf {
1878    if let Ok(exe) = std::env::current_exe() {
1879        if let Ok(canonical) = exe.canonicalize() {
1880            let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1881            if !is_in_cargo_target && canonical.exists() {
1882                return canonical;
1883            }
1884        }
1885    }
1886
1887    if let Ok(home) = std::env::var("HOME") {
1888        let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1889        if local_bin.parent().is_some_and(std::path::Path::exists) {
1890            return local_bin;
1891        }
1892    }
1893
1894    std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1895}
1896
1897fn spawn_proxy_if_needed() {
1898    use std::net::TcpStream;
1899    use std::time::Duration;
1900
1901    let port = crate::proxy_setup::default_port();
1902    let already_running = TcpStream::connect_timeout(
1903        &format!("127.0.0.1:{port}").parse().unwrap(),
1904        Duration::from_millis(200),
1905    )
1906    .is_ok();
1907
1908    if already_running {
1909        tracing::debug!("proxy already running on port {port}");
1910        return;
1911    }
1912
1913    let binary = std::env::current_exe().map_or_else(
1914        |_| "lean-ctx".to_string(),
1915        |p| p.to_string_lossy().to_string(),
1916    );
1917
1918    match std::process::Command::new(&binary)
1919        .args(["proxy", "start", &format!("--port={port}")])
1920        .stdin(std::process::Stdio::null())
1921        .stdout(std::process::Stdio::null())
1922        .stderr(std::process::Stdio::null())
1923        .spawn()
1924    {
1925        Ok(_) => tracing::info!("auto-started proxy on port {port}"),
1926        Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
1927    }
1928}
1929
1930fn resolve_worker_threads(parallelism: usize) -> usize {
1931    std::env::var("LEAN_CTX_WORKER_THREADS")
1932        .ok()
1933        .and_then(|v| v.parse::<usize>().ok())
1934        .unwrap_or_else(|| parallelism.clamp(1, 4))
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939    use super::*;
1940
1941    #[test]
1942    fn worker_threads_default_clamps_low() {
1943        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1944        assert_eq!(resolve_worker_threads(1), 1);
1945    }
1946
1947    #[test]
1948    fn worker_threads_default_clamps_high() {
1949        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1950        assert_eq!(resolve_worker_threads(32), 4);
1951    }
1952
1953    #[test]
1954    fn worker_threads_default_passthrough() {
1955        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1956        assert_eq!(resolve_worker_threads(3), 3);
1957    }
1958
1959    #[test]
1960    fn worker_threads_env_override() {
1961        std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
1962        assert_eq!(resolve_worker_threads(2), 12);
1963        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1964    }
1965
1966    #[test]
1967    fn worker_threads_env_invalid_falls_back() {
1968        std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
1969        assert_eq!(resolve_worker_threads(3), 3);
1970        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1971    }
1972}