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            "harden" => {
1247                super::harden::run(&rest);
1248                return;
1249            }
1250            "export-rules" => {
1251                super::export_rules::run(&rest);
1252                return;
1253            }
1254            "gotchas" | "bugs" => {
1255                super::cloud::cmd_gotchas(&rest);
1256                return;
1257            }
1258            "learn" => {
1259                super::cmd_learn(&rest);
1260                return;
1261            }
1262            "buddy" | "pet" => {
1263                super::cloud::cmd_buddy(&rest);
1264                return;
1265            }
1266            "hook" => {
1267                hook_handlers::mark_hook_environment();
1268                hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1269                let action = rest.first().map_or("help", std::string::String::as_str);
1270                match action {
1271                    "rewrite" => hook_handlers::handle_rewrite(),
1272                    "redirect" => hook_handlers::handle_redirect(),
1273                    "observe" => hook_handlers::handle_observe(),
1274                    "copilot" => hook_handlers::handle_copilot(),
1275                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1276                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
1277                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1278                    _ => {
1279                        eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1280                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1281                        std::process::exit(1);
1282                    }
1283                }
1284                return;
1285            }
1286            "report-issue" | "report" => {
1287                report::run(&rest);
1288                return;
1289            }
1290            "uninstall" => {
1291                let dry_run = rest.iter().any(|a| a == "--dry-run");
1292                uninstall::run(dry_run);
1293                return;
1294            }
1295            "bypass" => {
1296                if rest.is_empty() {
1297                    eprintln!("Usage: lean-ctx bypass \"command\"");
1298                    eprintln!("Runs the command with zero compression (raw passthrough).");
1299                    std::process::exit(1);
1300                }
1301                let command = if rest.len() == 1 {
1302                    rest[0].clone()
1303                } else {
1304                    shell::join_command(&args[2..])
1305                };
1306                std::env::set_var("LEAN_CTX_RAW", "1");
1307                let code = shell::exec(&command);
1308                std::process::exit(code);
1309            }
1310            "safety-levels" | "safety" => {
1311                println!("{}", core::compression_safety::format_safety_table());
1312                return;
1313            }
1314            "cheat" | "cheatsheet" | "cheat-sheet" => {
1315                super::cmd_cheatsheet();
1316                return;
1317            }
1318            "login" => {
1319                super::cloud::cmd_login(&rest);
1320                return;
1321            }
1322            "register" => {
1323                super::cloud::cmd_register(&rest);
1324                return;
1325            }
1326            "forgot-password" => {
1327                super::cloud::cmd_forgot_password(&rest);
1328                return;
1329            }
1330            "sync" => {
1331                super::cloud::cmd_sync();
1332                return;
1333            }
1334            "contribute" => {
1335                super::cloud::cmd_contribute();
1336                return;
1337            }
1338            "cloud" => {
1339                super::cloud::cmd_cloud(&rest);
1340                return;
1341            }
1342            "upgrade" => {
1343                super::cloud::cmd_upgrade();
1344                return;
1345            }
1346            "--version" | "-V" => {
1347                println!("{}", core::integrity::origin_line());
1348                return;
1349            }
1350            "--help" | "-h" => {
1351                print_help();
1352                return;
1353            }
1354            "mcp" => {}
1355            _ => {
1356                tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1357                print_help();
1358                std::process::exit(1);
1359            }
1360        }
1361    }
1362
1363    if let Err(e) = run_mcp_server() {
1364        tracing::error!("lean-ctx: {e}");
1365        std::process::exit(1);
1366    }
1367}
1368
1369fn passthrough(command: &str) -> ! {
1370    let (shell, flag) = shell::shell_and_flag();
1371    let status = std::process::Command::new(&shell)
1372        .arg(&flag)
1373        .arg(command)
1374        .env("LEAN_CTX_ACTIVE", "1")
1375        .status()
1376        .map_or(127, |s| s.code().unwrap_or(1));
1377    std::process::exit(status);
1378}
1379
1380fn run_async<F: std::future::Future>(future: F) -> F::Output {
1381    tokio::runtime::Runtime::new()
1382        .expect("failed to create async runtime")
1383        .block_on(future)
1384}
1385
1386fn run_mcp_server() -> Result<()> {
1387    use rmcp::ServiceExt;
1388
1389    std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1390
1391    crate::core::startup_guard::crash_loop_backoff("mcp-server");
1392
1393    // Concurrency hardening:
1394    // - Smooths "thundering herd" MCP startups (multiple agent sessions).
1395    // - Limits Tokio worker/blocking threads to avoid host degradation.
1396    // - LEAN_CTX_WORKER_THREADS overrides the default for environments
1397    //   with many concurrent subagents (e.g. parallel review pipelines).
1398    let startup_lock = crate::core::startup_guard::try_acquire_lock(
1399        "mcp-startup",
1400        std::time::Duration::from_secs(3),
1401        std::time::Duration::from_secs(30),
1402    );
1403
1404    let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1405    let worker_threads = resolve_worker_threads(parallelism);
1406    let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1407
1408    let rt = tokio::runtime::Builder::new_multi_thread()
1409        .worker_threads(worker_threads)
1410        .max_blocking_threads(max_blocking_threads)
1411        .enable_all()
1412        .build()?;
1413
1414    let server = tools::create_server();
1415    drop(startup_lock);
1416
1417    // Auto-start proxy in background so the dashboard gets exact token data.
1418    spawn_proxy_if_needed();
1419
1420    rt.block_on(async {
1421        core::logging::init_mcp_logging();
1422        core::protocol::set_mcp_context(true);
1423
1424        tracing::info!(
1425            "lean-ctx v{} MCP server starting",
1426            env!("CARGO_PKG_VERSION")
1427        );
1428
1429        let transport =
1430            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1431        let server_handle = server.clone();
1432        let service = match server.serve(transport).await {
1433            Ok(s) => s,
1434            Err(e) => {
1435                let msg = e.to_string();
1436                if msg.contains("expect initialized")
1437                    || msg.contains("context canceled")
1438                    || msg.contains("broken pipe")
1439                {
1440                    tracing::debug!("Client disconnected before init: {msg}");
1441                    return Ok(());
1442                }
1443                return Err(e.into());
1444            }
1445        };
1446        match service.waiting().await {
1447            Ok(reason) => {
1448                tracing::info!("MCP server stopped: {reason:?}");
1449            }
1450            Err(e) => {
1451                let msg = e.to_string();
1452                if msg.contains("broken pipe")
1453                    || msg.contains("connection reset")
1454                    || msg.contains("context canceled")
1455                {
1456                    tracing::info!("MCP server: transport closed ({msg})");
1457                } else {
1458                    tracing::error!("MCP server error: {msg}");
1459                }
1460            }
1461        }
1462
1463        server_handle.shutdown().await;
1464
1465        core::stats::flush();
1466        core::heatmap::flush();
1467        core::mode_predictor::ModePredictor::flush();
1468        core::feedback::FeedbackStore::flush();
1469
1470        Ok(())
1471    })
1472}
1473
1474fn print_help() {
1475    println!(
1476        "lean-ctx {version} — Context Runtime for AI Agents
1477
147860+ compression patterns | 51 MCP tools | 10 read modes | Context Continuity Protocol
1479
1480USAGE:
1481    lean-ctx                       Start MCP server (stdio)
1482    lean-ctx serve                 Start MCP server (Streamable HTTP)
1483    lean-ctx serve --daemon        Start as background daemon (Unix Domain Socket)
1484    lean-ctx serve --stop          Stop running daemon
1485    lean-ctx serve --status        Show daemon status
1486    lean-ctx -t \"command\"          Track command (full output + stats, no compression)
1487    lean-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
1488    lean-ctx -c --raw \"command\"    Execute without compression (full output)
1489    lean-ctx exec \"command\"        Same as -c
1490    lean-ctx shell                 Interactive shell with compression
1491
1492COMMANDS:
1493    gain                           Visual dashboard (colors, bars, sparklines, USD)
1494    gain --live                    Live mode: auto-refreshes every 1s in-place
1495    gain --graph                   30-day savings chart
1496    gain --daily                   Bordered day-by-day table with USD
1497    gain --json                    Raw JSON export of all stats
1498         token-report [--json]          Token + memory report (project + session + CEP)
1499    pack --pr                      PR Context Pack (changed files, impact, tests, artifacts)
1500    index <status|build|build-full|watch>  Codebase index utilities
1501    cep                            CEP impact report (score trends, cache, modes)
1502    watch                          Live TUI dashboard (real-time event stream)
1503    dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1504    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
1505    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
1506    proxy status                   Show proxy statistics
1507    cache [list|clear|stats]       Show/manage file read cache
1508    wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1509    sessions [list|show|cleanup]   Manage CCP sessions (~/.lean-ctx/sessions/)
1510    benchmark run [path] [--json]  Run real benchmark on project files
1511    benchmark report [path]        Generate shareable Markdown report
1512    cheatsheet                     Command cheat sheet & workflow quick reference
1513    setup                          One-command setup: shell + editor + verify
1514    install --repair [--json]      Premium repair: merge-based setup refresh (no deletes)
1515    bootstrap                      Non-interactive setup + fix (zero-config)
1516    status [--json]                Show setup + MCP + rules status
1517    init [--global]                Install shell aliases (zsh/bash/fish/PowerShell)
1518    init --agent <name>            Configure MCP for specific editor/agent
1519    read <file> [-m mode]          Read file with compression
1520    diff <file1> <file2>           Compressed file diff
1521    grep <pattern> [path]          Search with compressed output
1522    find <pattern> [path]          Find files with compressed output
1523    ls [path]                      Directory listing with compression
1524    deps [path]                    Show project dependencies
1525    discover                       Find uncompressed commands in shell history
1526    ghost [--json]                 Ghost Token report: find hidden token waste
1527    filter [list|validate|init]    Manage custom compression filters (~/.lean-ctx/filters/)
1528    session                        Show adoption statistics
1529    session task <desc>            Set current task
1530    session finding <summary>      Record a finding
1531    session save                   Save current session
1532    session load [id]              Load session (latest if no ID)
1533    knowledge remember <value> --category <c> --key <k>   Store a fact
1534    knowledge recall [query] [--category <c>]             Retrieve facts
1535    knowledge search <query>       Cross-project knowledge search
1536    knowledge export [--format json|jsonl|simple] [--output <path>]  Export knowledge
1537    knowledge import <path> [--merge replace|append|skip-existing]   Import knowledge
1538    knowledge remove --category <c> --key <k>             Remove a fact
1539    knowledge status               Knowledge base summary
1540    overview [task]                Project overview (task-contextualized if given)
1541    compress [--signatures]        Context compression checkpoint
1542    config                         Show/edit configuration (~/.lean-ctx/config.toml)
1543    profile [list|show|diff|create|set]  Manage context profiles
1544    theme [list|set|export|import] Customize terminal colors and themes
1545    tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1546    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
1547    slow-log [list|clear]          Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1548    update [--check]               Self-update lean-ctx binary from GitHub Releases
1549    stop                           Stop ALL lean-ctx processes (daemon, proxy, orphans)
1550    restart                        Restart daemon (applies config.toml changes)
1551    dev-install                    Build release + atomic install + restart (for development)
1552    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1553    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
1554    doctor integrations [--json]   Integration health checks (Cursor/Claude Code)
1555    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
1556    smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1557                                   Code smell detection (Property Graph, 8 rules)
1558    control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1559    plan <task> [--budget=N]       Context planning (optimal Phi-scored context plan)
1560    compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1561    uninstall                      Remove shell hook, MCP configs, and data directory
1562
1563SHELL HOOK PATTERNS (95+):
1564    git       status, log, diff, add, commit, push, pull, fetch, clone,
1565              branch, checkout, switch, merge, stash, tag, reset, remote
1566    docker    build, ps, images, logs, compose, exec, network
1567    npm/pnpm  install, test, run, list, outdated, audit
1568    cargo     build, test, check, clippy
1569    gh        pr list/view/create, issue list/view, run list/view
1570    kubectl   get pods/services/deployments, logs, describe, apply
1571    python    pip install/list/outdated, ruff check/format, poetry, uv
1572    linters   eslint, biome, prettier, golangci-lint
1573    builds    tsc, next build, vite build
1574    ruby      rubocop, bundle install/update, rake test, rails test
1575    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
1576    iac       terraform, make, maven, gradle, dotnet, flutter, dart
1577    utils     curl, grep/rg, find, ls, wget, env
1578    data      JSON schema extraction, log deduplication
1579
1580READ MODES:
1581    auto                           Auto-select optimal mode (default)
1582    full                           Full content (cached re-reads = 13 tokens)
1583    map                            Dependency graph + API signatures
1584    signatures                     tree-sitter AST extraction (18 languages)
1585    task                           Task-relevant filtering (requires ctx_session task)
1586    reference                      One-line reference stub (cheap cache key)
1587    aggressive                     Syntax-stripped content
1588    entropy                        Shannon entropy filtered
1589    diff                           Changed lines only
1590    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
1591
1592ENVIRONMENT:
1593    LEAN_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
1594    LEAN_CTX_ENABLED=0             Prevent shell hook auto-start (lean-ctx-on still works)
1595    LEAN_CTX_RAW=1                 Same as --raw for current command
1596    LEAN_CTX_AUTONOMY=false        Disable autonomous features
1597    LEAN_CTX_COMPRESS=1            Force compression (even for excluded commands)
1598
1599OPTIONS:
1600    --version, -V                  Show version
1601    --help, -h                     Show this help
1602
1603EXAMPLES:
1604    lean-ctx -c \"git status\"       Compressed git output
1605    lean-ctx -c \"kubectl get pods\" Compressed k8s output
1606    lean-ctx -c \"gh pr list\"       Compressed GitHub CLI output
1607    lean-ctx gain                  Visual terminal dashboard
1608    lean-ctx gain --live           Live auto-updating terminal dashboard
1609    lean-ctx gain --graph          30-day savings chart
1610    lean-ctx gain --daily          Day-by-day breakdown with USD
1611         lean-ctx token-report --json   Machine-readable token + memory report
1612    lean-ctx dashboard             Open web dashboard at localhost:3333
1613    lean-ctx dashboard --host=0.0.0.0  Bind to all interfaces (remote access)
1614    lean-ctx gain --wrapped        Wrapped report card (recommended)
1615    lean-ctx gain --wrapped --period=month  Monthly Wrapped report card
1616    lean-ctx sessions list         List all CCP sessions
1617    lean-ctx sessions show         Show latest session state
1618    lean-ctx discover              Find missed savings in shell history
1619    lean-ctx setup                 One-command setup (shell + editors + verify)
1620    lean-ctx install --repair      Premium repair path (non-interactive, merge-based)
1621    lean-ctx bootstrap             Non-interactive setup + fix (zero-config)
1622    lean-ctx bootstrap --json      Machine-readable bootstrap report
1623    lean-ctx init --global         Install shell aliases (includes lean-ctx-on/off/mode/status)
1624    lean-ctx-on                    Enable shell aliases in track mode (full output + stats)
1625    lean-ctx-off                   Disable all shell aliases
1626    lean-ctx-mode track            Track mode: full output, stats recorded (default)
1627    lean-ctx-mode compress         Compress mode: all output compressed (power users)
1628    lean-ctx-mode off              Same as lean-ctx-off
1629    lean-ctx-status                Show whether compression is active
1630    lean-ctx init --agent pi       Install Pi Coding Agent extension
1631    lean-ctx doctor                Check PATH, config, MCP, and dashboard port
1632    lean-ctx doctor integrations   Premium integration checks (Cursor/Claude Code)
1633    lean-ctx doctor --fix --json   Repair + machine-readable report
1634    lean-ctx status --json         Machine-readable current status
1635    lean-ctx session task \"implement auth\"
1636    lean-ctx session finding \"auth.rs:42 — missing validation\"
1637    lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1638    lean-ctx knowledge recall \"authentication\"
1639    lean-ctx knowledge search \"database migration\"
1640    lean-ctx overview \"refactor auth module\"
1641    lean-ctx compress --signatures
1642    lean-ctx read src/main.rs -m map
1643    lean-ctx grep \"pub fn\" src/
1644    lean-ctx deps .
1645
1646CLOUD:
1647    cloud status                   Show cloud connection status
1648    login <email>                  Log into existing LeanCTX Cloud account
1649    register <email>               Create a new LeanCTX Cloud account
1650    forgot-password <email>        Send password reset email
1651    sync                           Upload local stats to cloud dashboard
1652    contribute                     Share anonymized compression data
1653
1654TROUBLESHOOTING:
1655    Commands broken?     lean-ctx-off             (fixes current session)
1656    Permanent fix?       lean-ctx uninstall       (removes all hooks)
1657    Manual fix?          Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1658    Binary missing?      Aliases auto-fallback to original commands (safe)
1659    Preview init?        lean-ctx init --global --dry-run
1660
1661WEBSITE: https://leanctx.com
1662GITHUB:  https://github.com/yvgude/lean-ctx
1663",
1664        version = env!("CARGO_PKG_VERSION"),
1665    );
1666}
1667
1668fn cmd_stop() {
1669    use crate::daemon;
1670    use crate::ipc;
1671
1672    eprintln!("Stopping all lean-ctx processes…");
1673
1674    // 1. Unload LaunchAgent/systemd first to prevent respawning
1675    crate::proxy_autostart::stop();
1676    eprintln!("  Unloaded autostart (LaunchAgent/systemd).");
1677
1678    // 2. Stop daemon via IPC
1679    if let Err(e) = daemon::stop_daemon() {
1680        eprintln!("  Warning: daemon stop: {e}");
1681    }
1682
1683    // 3. SIGTERM all remaining lean-ctx processes
1684    let killed = ipc::process::kill_all_by_name("lean-ctx");
1685    if killed > 0 {
1686        eprintln!("  Sent SIGTERM to {killed} process(es).");
1687    }
1688
1689    std::thread::sleep(std::time::Duration::from_millis(500));
1690
1691    // 4. Force-kill stragglers (but never MCP servers — IDE will respawn them)
1692    let remaining = ipc::process::find_killable_pids("lean-ctx");
1693    if !remaining.is_empty() {
1694        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1695        for &pid in &remaining {
1696            let _ = ipc::process::force_kill(pid);
1697        }
1698        std::thread::sleep(std::time::Duration::from_millis(300));
1699    }
1700
1701    daemon::cleanup_daemon_files();
1702
1703    let final_check = ipc::process::find_killable_pids("lean-ctx");
1704    if final_check.is_empty() {
1705        eprintln!("  ✓ All lean-ctx processes stopped.");
1706    } else {
1707        eprintln!(
1708            "  ✗ {} process(es) could not be killed: {:?}",
1709            final_check.len(),
1710            final_check
1711        );
1712        eprintln!(
1713            "    Try: sudo kill -9 {}",
1714            final_check
1715                .iter()
1716                .map(std::string::ToString::to_string)
1717                .collect::<Vec<_>>()
1718                .join(" ")
1719        );
1720        std::process::exit(1);
1721    }
1722}
1723
1724fn cmd_restart() {
1725    use crate::daemon;
1726    use crate::ipc;
1727
1728    eprintln!("Restarting lean-ctx…");
1729
1730    // Stop autostart first to prevent respawning during restart
1731    crate::proxy_autostart::stop();
1732
1733    if let Err(e) = daemon::stop_daemon() {
1734        eprintln!("  Warning: daemon stop: {e}");
1735    }
1736
1737    let orphans = ipc::process::kill_all_by_name("lean-ctx");
1738    if orphans > 0 {
1739        eprintln!("  Terminated {orphans} orphan process(es).");
1740    }
1741
1742    std::thread::sleep(std::time::Duration::from_millis(500));
1743
1744    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1745    if !remaining.is_empty() {
1746        eprintln!(
1747            "  Force-killing {} stubborn process(es): {:?}",
1748            remaining.len(),
1749            remaining
1750        );
1751        for &pid in &remaining {
1752            let _ = ipc::process::force_kill(pid);
1753        }
1754        std::thread::sleep(std::time::Duration::from_millis(300));
1755    }
1756
1757    daemon::cleanup_daemon_files();
1758
1759    // Re-enable autostart
1760    crate::proxy_autostart::start();
1761
1762    match daemon::start_daemon(&[]) {
1763        Ok(()) => eprintln!("  ✓ Daemon restarted. Config changes are now active."),
1764        Err(e) => {
1765            eprintln!("  ✗ Daemon start failed: {e}");
1766            std::process::exit(1);
1767        }
1768    }
1769}
1770
1771fn cmd_dev_install() {
1772    use crate::ipc;
1773
1774    let cargo_root = find_cargo_project_root();
1775    let Some(cargo_root) = cargo_root else {
1776        eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1777        std::process::exit(1);
1778    };
1779
1780    eprintln!("Building release binary…");
1781    let build = std::process::Command::new("cargo")
1782        .args(["build", "--release"])
1783        .current_dir(&cargo_root)
1784        .status();
1785
1786    match build {
1787        Ok(s) if s.success() => {}
1788        Ok(s) => {
1789            eprintln!("  Build failed with exit code {}", s.code().unwrap_or(-1));
1790            std::process::exit(1);
1791        }
1792        Err(e) => {
1793            eprintln!("  Build failed: {e}");
1794            std::process::exit(1);
1795        }
1796    }
1797
1798    let built_binary = cargo_root.join("target/release/lean-ctx");
1799    if !built_binary.exists() {
1800        eprintln!(
1801            "  Error: Built binary not found at {}",
1802            built_binary.display()
1803        );
1804        std::process::exit(1);
1805    }
1806
1807    let install_path = resolve_install_path();
1808    eprintln!("Installing to {}…", install_path.display());
1809
1810    eprintln!("  Stopping all lean-ctx processes…");
1811    crate::proxy_autostart::stop();
1812    let _ = crate::daemon::stop_daemon();
1813    ipc::process::kill_all_by_name("lean-ctx");
1814    std::thread::sleep(std::time::Duration::from_millis(500));
1815
1816    let remaining = ipc::process::find_pids_by_name("lean-ctx");
1817    if !remaining.is_empty() {
1818        eprintln!("  Force-killing {} stubborn process(es)…", remaining.len());
1819        for &pid in &remaining {
1820            let _ = ipc::process::force_kill(pid);
1821        }
1822        std::thread::sleep(std::time::Duration::from_millis(500));
1823    }
1824
1825    let old_path = install_path.with_extension("old");
1826    if install_path.exists() {
1827        if let Err(e) = std::fs::rename(&install_path, &old_path) {
1828            eprintln!("  Warning: rename existing binary: {e}");
1829        }
1830    }
1831
1832    match std::fs::copy(&built_binary, &install_path) {
1833        Ok(_) => {
1834            let _ = std::fs::remove_file(&old_path);
1835            #[cfg(unix)]
1836            {
1837                use std::os::unix::fs::PermissionsExt;
1838                let _ =
1839                    std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1840            }
1841            eprintln!("  ✓ Binary installed.");
1842        }
1843        Err(e) => {
1844            eprintln!("  Error: copy failed: {e}");
1845            if old_path.exists() {
1846                let _ = std::fs::rename(&old_path, &install_path);
1847                eprintln!("  Rolled back to previous binary.");
1848            }
1849            std::process::exit(1);
1850        }
1851    }
1852
1853    let version = std::process::Command::new(&install_path)
1854        .arg("--version")
1855        .output()
1856        .map_or_else(
1857            |_| "unknown".to_string(),
1858            |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1859        );
1860
1861    eprintln!("  ✓ dev-install complete: {version}");
1862
1863    eprintln!("  Re-enabling autostart…");
1864    crate::proxy_autostart::start();
1865
1866    eprintln!("  Starting daemon…");
1867    match crate::daemon::start_daemon(&[]) {
1868        Ok(()) => {}
1869        Err(e) => eprintln!("  Warning: daemon start: {e} (will be started by editor)"),
1870    }
1871}
1872
1873fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1874    let mut dir = std::env::current_dir().ok()?;
1875    loop {
1876        if dir.join("Cargo.toml").exists() {
1877            return Some(dir);
1878        }
1879        if !dir.pop() {
1880            return None;
1881        }
1882    }
1883}
1884
1885fn resolve_install_path() -> std::path::PathBuf {
1886    if let Ok(exe) = std::env::current_exe() {
1887        if let Ok(canonical) = exe.canonicalize() {
1888            let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1889            if !is_in_cargo_target && canonical.exists() {
1890                return canonical;
1891            }
1892        }
1893    }
1894
1895    if let Ok(home) = std::env::var("HOME") {
1896        let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1897        if local_bin.parent().is_some_and(std::path::Path::exists) {
1898            return local_bin;
1899        }
1900    }
1901
1902    std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1903}
1904
1905fn spawn_proxy_if_needed() {
1906    use std::net::TcpStream;
1907    use std::time::Duration;
1908
1909    let port = crate::proxy_setup::default_port();
1910    let already_running = TcpStream::connect_timeout(
1911        &format!("127.0.0.1:{port}").parse().unwrap(),
1912        Duration::from_millis(200),
1913    )
1914    .is_ok();
1915
1916    if already_running {
1917        tracing::debug!("proxy already running on port {port}");
1918        return;
1919    }
1920
1921    let binary = std::env::current_exe().map_or_else(
1922        |_| "lean-ctx".to_string(),
1923        |p| p.to_string_lossy().to_string(),
1924    );
1925
1926    match std::process::Command::new(&binary)
1927        .args(["proxy", "start", &format!("--port={port}")])
1928        .stdin(std::process::Stdio::null())
1929        .stdout(std::process::Stdio::null())
1930        .stderr(std::process::Stdio::null())
1931        .spawn()
1932    {
1933        Ok(_) => tracing::info!("auto-started proxy on port {port}"),
1934        Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
1935    }
1936}
1937
1938fn resolve_worker_threads(parallelism: usize) -> usize {
1939    std::env::var("LEAN_CTX_WORKER_THREADS")
1940        .ok()
1941        .and_then(|v| v.parse::<usize>().ok())
1942        .unwrap_or_else(|| parallelism.clamp(1, 4))
1943}
1944
1945#[cfg(test)]
1946mod tests {
1947    use super::*;
1948
1949    #[test]
1950    fn worker_threads_default_clamps_low() {
1951        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1952        assert_eq!(resolve_worker_threads(1), 1);
1953    }
1954
1955    #[test]
1956    fn worker_threads_default_clamps_high() {
1957        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1958        assert_eq!(resolve_worker_threads(32), 4);
1959    }
1960
1961    #[test]
1962    fn worker_threads_default_passthrough() {
1963        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1964        assert_eq!(resolve_worker_threads(3), 3);
1965    }
1966
1967    #[test]
1968    fn worker_threads_env_override() {
1969        std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
1970        assert_eq!(resolve_worker_threads(2), 12);
1971        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1972    }
1973
1974    #[test]
1975    fn worker_threads_env_invalid_falls_back() {
1976        std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
1977        assert_eq!(resolve_worker_threads(3), 3);
1978        std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1979    }
1980}