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
10 if args.len() > 1 {
11 let rest = args[2..].to_vec();
12
13 match args[1].as_str() {
14 "-c" | "exec" => {
15 let raw = rest.first().is_some_and(|a| a == "--raw");
16 let cmd_args = if raw { &args[3..] } else { &args[2..] };
17 let command = if cmd_args.len() == 1 {
18 cmd_args[0].clone()
19 } else {
20 shell::join_command(cmd_args)
21 };
22 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
23 || std::env::var("LEAN_CTX_DISABLED").is_ok()
24 {
25 passthrough(&command);
26 }
27 if raw {
28 std::env::set_var("LEAN_CTX_RAW", "1");
29 } else {
30 std::env::set_var("LEAN_CTX_COMPRESS", "1");
31 }
32 let code = shell::exec(&command);
33 core::stats::flush();
34 std::process::exit(code);
35 }
36 "-t" | "--track" => {
37 let cmd_args = &args[2..];
38 let code = if cmd_args.len() > 1 {
39 shell::exec_argv(cmd_args)
40 } else {
41 let command = cmd_args[0].clone();
42 if std::env::var("LEAN_CTX_ACTIVE").is_ok()
43 || std::env::var("LEAN_CTX_DISABLED").is_ok()
44 {
45 passthrough(&command);
46 }
47 shell::exec(&command)
48 };
49 core::stats::flush();
50 std::process::exit(code);
51 }
52 "shell" | "--shell" => {
53 shell::interactive();
54 return;
55 }
56 "gain" => {
57 if rest.iter().any(|a| a == "--reset") {
58 core::stats::reset_all();
59 println!("Stats reset. All token savings data cleared.");
60 return;
61 }
62 if rest.iter().any(|a| a == "--live" || a == "--watch") {
63 core::stats::gain_live();
64 return;
65 }
66 let model = rest.iter().enumerate().find_map(|(i, a)| {
67 if let Some(v) = a.strip_prefix("--model=") {
68 return Some(v.to_string());
69 }
70 if a == "--model" {
71 return rest.get(i + 1).cloned();
72 }
73 None
74 });
75 let period = rest
76 .iter()
77 .enumerate()
78 .find_map(|(i, a)| {
79 if let Some(v) = a.strip_prefix("--period=") {
80 return Some(v.to_string());
81 }
82 if a == "--period" {
83 return rest.get(i + 1).cloned();
84 }
85 None
86 })
87 .unwrap_or_else(|| "all".to_string());
88 let limit = rest
89 .iter()
90 .enumerate()
91 .find_map(|(i, a)| {
92 if let Some(v) = a.strip_prefix("--limit=") {
93 return v.parse::<usize>().ok();
94 }
95 if a == "--limit" {
96 return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
97 }
98 None
99 })
100 .unwrap_or(10);
101
102 if rest.iter().any(|a| a == "--graph") {
103 println!("{}", core::stats::format_gain_graph());
104 } else if rest.iter().any(|a| a == "--daily") {
105 println!("{}", core::stats::format_gain_daily());
106 } else if rest.iter().any(|a| a == "--json") {
107 println!(
108 "{}",
109 tools::ctx_gain::handle(
110 "json",
111 Some(&period),
112 model.as_deref(),
113 Some(limit)
114 )
115 );
116 } else if rest.iter().any(|a| a == "--score") {
117 println!(
118 "{}",
119 tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
120 );
121 } else if rest.iter().any(|a| a == "--cost") {
122 println!(
123 "{}",
124 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
125 );
126 } else if rest.iter().any(|a| a == "--tasks") {
127 println!(
128 "{}",
129 tools::ctx_gain::handle("tasks", None, None, Some(limit))
130 );
131 } else if rest.iter().any(|a| a == "--agents") {
132 println!(
133 "{}",
134 tools::ctx_gain::handle("agents", None, None, Some(limit))
135 );
136 } else if rest.iter().any(|a| a == "--heatmap") {
137 println!(
138 "{}",
139 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
140 );
141 } else if rest.iter().any(|a| a == "--wrapped") {
142 println!(
143 "{}",
144 tools::ctx_gain::handle(
145 "wrapped",
146 Some(&period),
147 model.as_deref(),
148 Some(limit)
149 )
150 );
151 } else if rest.iter().any(|a| a == "--pipeline") {
152 let stats_path = dirs::home_dir()
153 .unwrap_or_default()
154 .join(".lean-ctx")
155 .join("pipeline_stats.json");
156 if let Ok(data) = std::fs::read_to_string(&stats_path) {
157 if let Ok(stats) =
158 serde_json::from_str::<core::pipeline::PipelineStats>(&data)
159 {
160 println!("{}", stats.format_summary());
161 } else {
162 println!("No pipeline stats available yet (corrupt data).");
163 }
164 } else {
165 println!(
166 "No pipeline stats available yet. Use MCP tools to generate data."
167 );
168 }
169 } else if rest.iter().any(|a| a == "--deep") {
170 println!(
171 "{}\n{}\n{}\n{}\n{}",
172 tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
173 tools::ctx_gain::handle("tasks", None, None, Some(limit)),
174 tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
175 tools::ctx_gain::handle("agents", None, None, Some(limit)),
176 tools::ctx_gain::handle("heatmap", None, None, Some(limit))
177 );
178 } else {
179 println!("{}", core::stats::format_gain());
180 }
181 return;
182 }
183 "token-report" | "report-tokens" => {
184 let code = token_report::run_cli(&rest);
185 if code != 0 {
186 std::process::exit(code);
187 }
188 return;
189 }
190 "cep" => {
191 println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
192 return;
193 }
194 "dashboard" => {
195 let port = rest
196 .iter()
197 .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
198 .and_then(|p| p.parse().ok());
199 let host = rest
200 .iter()
201 .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
202 .map(String::from);
203 let project = rest
204 .iter()
205 .find_map(|p| p.strip_prefix("--project="))
206 .map(String::from);
207 if let Some(ref p) = project {
208 std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
209 }
210 run_async(dashboard::start(port, host));
211 return;
212 }
213 "team" => {
214 let sub = rest.first().map_or("help", std::string::String::as_str);
215 match sub {
216 "serve" => {
217 #[cfg(feature = "team-server")]
218 {
219 let cfg_path = rest
220 .iter()
221 .enumerate()
222 .find_map(|(i, a)| {
223 if let Some(v) = a.strip_prefix("--config=") {
224 return Some(v.to_string());
225 }
226 if a == "--config" {
227 return rest.get(i + 1).cloned();
228 }
229 None
230 })
231 .unwrap_or_default();
232
233 if cfg_path.trim().is_empty() {
234 eprintln!("Usage: lean-ctx team serve --config <path>");
235 std::process::exit(1);
236 }
237
238 let cfg = crate::http_server::team::TeamServerConfig::load(
239 std::path::Path::new(&cfg_path),
240 )
241 .unwrap_or_else(|e| {
242 eprintln!("Invalid team config: {e}");
243 std::process::exit(1);
244 });
245
246 if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
247 tracing::error!("Team server error: {e}");
248 std::process::exit(1);
249 }
250 return;
251 }
252 #[cfg(not(feature = "team-server"))]
253 {
254 eprintln!("lean-ctx team serve is not available in this build");
255 std::process::exit(1);
256 }
257 }
258 "token" => {
259 let action = rest.get(1).map_or("help", std::string::String::as_str);
260 if action == "create" {
261 #[cfg(feature = "team-server")]
262 {
263 let args = &rest[2..];
264 let cfg_path = args
265 .iter()
266 .enumerate()
267 .find_map(|(i, a)| {
268 if let Some(v) = a.strip_prefix("--config=") {
269 return Some(v.to_string());
270 }
271 if a == "--config" {
272 return args.get(i + 1).cloned();
273 }
274 None
275 })
276 .unwrap_or_default();
277 let token_id = args
278 .iter()
279 .enumerate()
280 .find_map(|(i, a)| {
281 if let Some(v) = a.strip_prefix("--id=") {
282 return Some(v.to_string());
283 }
284 if a == "--id" {
285 return args.get(i + 1).cloned();
286 }
287 None
288 })
289 .unwrap_or_default();
290 let scopes_csv = args
291 .iter()
292 .enumerate()
293 .find_map(|(i, a)| {
294 if let Some(v) = a.strip_prefix("--scopes=") {
295 return Some(v.to_string());
296 }
297 if let Some(v) = a.strip_prefix("--scope=") {
298 return Some(v.to_string());
299 }
300 if a == "--scopes" || a == "--scope" {
301 return args.get(i + 1).cloned();
302 }
303 None
304 })
305 .unwrap_or_default();
306
307 if cfg_path.trim().is_empty()
308 || token_id.trim().is_empty()
309 || scopes_csv.trim().is_empty()
310 {
311 eprintln!(
312 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
313 );
314 std::process::exit(1);
315 }
316
317 let cfg_p = std::path::PathBuf::from(&cfg_path);
318 let mut cfg = crate::http_server::team::TeamServerConfig::load(
319 cfg_p.as_path(),
320 )
321 .unwrap_or_else(|e| {
322 eprintln!("Invalid team config: {e}");
323 std::process::exit(1);
324 });
325
326 let mut scopes = Vec::new();
327 for part in scopes_csv.split(',') {
328 let p = part.trim().to_ascii_lowercase();
329 if p.is_empty() {
330 continue;
331 }
332 let scope = match p.as_str() {
333 "search" => crate::http_server::team::TeamScope::Search,
334 "graph" => crate::http_server::team::TeamScope::Graph,
335 "artifacts" => {
336 crate::http_server::team::TeamScope::Artifacts
337 }
338 "index" => crate::http_server::team::TeamScope::Index,
339 _ => {
340 eprintln!("Unknown scope: {p}");
341 std::process::exit(1);
342 }
343 };
344 if !scopes.contains(&scope) {
345 scopes.push(scope);
346 }
347 }
348 if scopes.is_empty() {
349 eprintln!("At least 1 scope is required");
350 std::process::exit(1);
351 }
352
353 let (token, hash) = crate::http_server::team::create_token()
354 .unwrap_or_else(|e| {
355 eprintln!("Token generation failed: {e}");
356 std::process::exit(1);
357 });
358
359 cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
360 id: token_id,
361 sha256_hex: hash,
362 scopes,
363 });
364
365 cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
366 eprintln!("Failed to write config: {e}");
367 std::process::exit(1);
368 });
369
370 println!("{token}");
371 return;
372 }
373
374 #[cfg(not(feature = "team-server"))]
375 {
376 eprintln!("lean-ctx team token is not available in this build");
377 std::process::exit(1);
378 }
379 }
380 eprintln!(
381 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
382 );
383 std::process::exit(1);
384 }
385 "sync" => {
386 #[cfg(feature = "team-server")]
387 {
388 let args = &rest[1..];
389 let cfg_path = args
390 .iter()
391 .enumerate()
392 .find_map(|(i, a)| {
393 if let Some(v) = a.strip_prefix("--config=") {
394 return Some(v.to_string());
395 }
396 if a == "--config" {
397 return args.get(i + 1).cloned();
398 }
399 None
400 })
401 .unwrap_or_default();
402 if cfg_path.trim().is_empty() {
403 eprintln!(
404 "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
405 );
406 std::process::exit(1);
407 }
408 let only_ws = args.iter().enumerate().find_map(|(i, a)| {
409 if let Some(v) = a.strip_prefix("--workspace=") {
410 return Some(v.to_string());
411 }
412 if let Some(v) = a.strip_prefix("--workspace-id=") {
413 return Some(v.to_string());
414 }
415 if a == "--workspace" || a == "--workspace-id" {
416 return args.get(i + 1).cloned();
417 }
418 None
419 });
420
421 let cfg = crate::http_server::team::TeamServerConfig::load(
422 std::path::Path::new(&cfg_path),
423 )
424 .unwrap_or_else(|e| {
425 eprintln!("Invalid team config: {e}");
426 std::process::exit(1);
427 });
428
429 for ws in &cfg.workspaces {
430 if let Some(ref only) = only_ws {
431 if ws.id != *only {
432 continue;
433 }
434 }
435 let git_dir = ws.root.join(".git");
436 if !git_dir.exists() {
437 eprintln!(
438 "workspace '{}' root is not a git repo: {}",
439 ws.id,
440 ws.root.display()
441 );
442 std::process::exit(1);
443 }
444 let status = std::process::Command::new("git")
445 .arg("-C")
446 .arg(&ws.root)
447 .args(["fetch", "--all", "--prune"])
448 .status()
449 .unwrap_or_else(|e| {
450 eprintln!(
451 "git fetch failed for workspace '{}': {e}",
452 ws.id
453 );
454 std::process::exit(1);
455 });
456 if !status.success() {
457 eprintln!(
458 "git fetch failed for workspace '{}' (exit={})",
459 ws.id,
460 status.code().unwrap_or(1)
461 );
462 std::process::exit(1);
463 }
464 }
465 return;
466 }
467 #[cfg(not(feature = "team-server"))]
468 {
469 eprintln!("lean-ctx team sync is not available in this build");
470 std::process::exit(1);
471 }
472 }
473 _ => {
474 eprintln!(
475 "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>]"
476 );
477 std::process::exit(1);
478 }
479 }
480 }
481 "serve" => {
482 #[cfg(feature = "http-server")]
483 {
484 let mut cfg = crate::http_server::HttpServerConfig::default();
485 let mut i = 0;
486 while i < rest.len() {
487 match rest[i].as_str() {
488 "--host" | "-H" => {
489 i += 1;
490 if i < rest.len() {
491 cfg.host.clone_from(&rest[i]);
492 }
493 }
494 arg if arg.starts_with("--host=") => {
495 cfg.host = arg["--host=".len()..].to_string();
496 }
497 "--port" | "-p" => {
498 i += 1;
499 if i < rest.len() {
500 if let Ok(p) = rest[i].parse::<u16>() {
501 cfg.port = p;
502 }
503 }
504 }
505 arg if arg.starts_with("--port=") => {
506 if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
507 cfg.port = p;
508 }
509 }
510 "--project-root" => {
511 i += 1;
512 if i < rest.len() {
513 cfg.project_root = std::path::PathBuf::from(&rest[i]);
514 }
515 }
516 arg if arg.starts_with("--project-root=") => {
517 cfg.project_root =
518 std::path::PathBuf::from(&arg["--project-root=".len()..]);
519 }
520 "--auth-token" => {
521 i += 1;
522 if i < rest.len() {
523 cfg.auth_token = Some(rest[i].clone());
524 }
525 }
526 arg if arg.starts_with("--auth-token=") => {
527 cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
528 }
529 "--stateful" => cfg.stateful_mode = true,
530 "--stateless" => cfg.stateful_mode = false,
531 "--json" => cfg.json_response = true,
532 "--sse" => cfg.json_response = false,
533 "--disable-host-check" => cfg.disable_host_check = true,
534 "--allowed-host" => {
535 i += 1;
536 if i < rest.len() {
537 cfg.allowed_hosts.push(rest[i].clone());
538 }
539 }
540 arg if arg.starts_with("--allowed-host=") => {
541 cfg.allowed_hosts
542 .push(arg["--allowed-host=".len()..].to_string());
543 }
544 "--max-body-bytes" => {
545 i += 1;
546 if i < rest.len() {
547 if let Ok(n) = rest[i].parse::<usize>() {
548 cfg.max_body_bytes = n;
549 }
550 }
551 }
552 arg if arg.starts_with("--max-body-bytes=") => {
553 if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
554 cfg.max_body_bytes = n;
555 }
556 }
557 "--max-concurrency" => {
558 i += 1;
559 if i < rest.len() {
560 if let Ok(n) = rest[i].parse::<usize>() {
561 cfg.max_concurrency = n;
562 }
563 }
564 }
565 arg if arg.starts_with("--max-concurrency=") => {
566 if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
567 cfg.max_concurrency = n;
568 }
569 }
570 "--max-rps" => {
571 i += 1;
572 if i < rest.len() {
573 if let Ok(n) = rest[i].parse::<u32>() {
574 cfg.max_rps = n;
575 }
576 }
577 }
578 arg if arg.starts_with("--max-rps=") => {
579 if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
580 cfg.max_rps = n;
581 }
582 }
583 "--rate-burst" => {
584 i += 1;
585 if i < rest.len() {
586 if let Ok(n) = rest[i].parse::<u32>() {
587 cfg.rate_burst = n;
588 }
589 }
590 }
591 arg if arg.starts_with("--rate-burst=") => {
592 if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
593 cfg.rate_burst = n;
594 }
595 }
596 "--request-timeout-ms" => {
597 i += 1;
598 if i < rest.len() {
599 if let Ok(n) = rest[i].parse::<u64>() {
600 cfg.request_timeout_ms = n;
601 }
602 }
603 }
604 arg if arg.starts_with("--request-timeout-ms=") => {
605 if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
606 cfg.request_timeout_ms = n;
607 }
608 }
609 "--help" | "-h" => {
610 eprintln!(
611 "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR]\\n\\
612 \\n\\
613 Options:\\n\\
614 --host, -H Bind host (default: 127.0.0.1)\\n\\
615 --port, -p Bind port (default: 8080)\\n\\
616 --project-root Resolve relative paths against this root (default: cwd)\\n\\
617 --auth-token Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
618 --stateful/--stateless Streamable HTTP session mode (default: stateless)\\n\\
619 --json/--sse Response framing in stateless mode (default: json)\\n\\
620 --max-body-bytes Max request body size in bytes (default: 2097152)\\n\\
621 --max-concurrency Max concurrent requests (default: 32)\\n\\
622 --max-rps Max requests/sec (global, default: 50)\\n\\
623 --rate-burst Rate limiter burst (global, default: 100)\\n\\
624 --request-timeout-ms REST tool-call timeout (default: 30000)\\n\\
625 --allowed-host Add allowed Host header (repeatable)\\n\\
626 --disable-host-check Disable Host header validation (unsafe)"
627 );
628 return;
629 }
630 _ => {}
631 }
632 i += 1;
633 }
634
635 if cfg.auth_token.is_none() {
636 if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
637 if !v.trim().is_empty() {
638 cfg.auth_token = Some(v);
639 }
640 }
641 }
642
643 if let Err(e) = run_async(crate::http_server::serve(cfg)) {
644 tracing::error!("HTTP server error: {e}");
645 std::process::exit(1);
646 }
647 return;
648 }
649 #[cfg(not(feature = "http-server"))]
650 {
651 eprintln!("lean-ctx serve is not available in this build");
652 std::process::exit(1);
653 }
654 }
655 "watch" => {
656 if let Err(e) = tui::run() {
657 tracing::error!("TUI error: {e}");
658 std::process::exit(1);
659 }
660 return;
661 }
662 "proxy" => {
663 #[cfg(feature = "http-server")]
664 {
665 let sub = rest.first().map_or("help", std::string::String::as_str);
666 match sub {
667 "start" => {
668 let port: u16 = rest
669 .iter()
670 .find_map(|p| {
671 p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
672 })
673 .and_then(|p| p.parse().ok())
674 .unwrap_or(4444);
675 let autostart = rest.iter().any(|a| a == "--autostart");
676 if autostart {
677 crate::proxy_autostart::install(port, false);
678 return;
679 }
680 if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
681 tracing::error!("Proxy error: {e}");
682 std::process::exit(1);
683 }
684 }
685 "stop" => {
686 match ureq::get(&format!(
687 "http://127.0.0.1:{}/health",
688 rest.iter()
689 .find_map(|p| p.strip_prefix("--port="))
690 .and_then(|p| p.parse::<u16>().ok())
691 .unwrap_or(4444)
692 ))
693 .call()
694 {
695 Ok(_) => {
696 println!("Proxy is running. Use Ctrl+C or kill the process.");
697 }
698 Err(_) => {
699 println!("No proxy running on that port.");
700 }
701 }
702 }
703 "status" => {
704 let port: u16 = rest
705 .iter()
706 .find_map(|p| p.strip_prefix("--port="))
707 .and_then(|p| p.parse().ok())
708 .unwrap_or(4444);
709 if let Ok(resp) =
710 ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
711 {
712 let body = resp.into_body().read_to_string().unwrap_or_default();
713 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
714 println!("lean-ctx proxy status:");
715 println!(" Requests: {}", v["requests_total"]);
716 println!(" Compressed: {}", v["requests_compressed"]);
717 println!(" Tokens saved: {}", v["tokens_saved"]);
718 println!(
719 " Compression: {}%",
720 v["compression_ratio_pct"].as_str().unwrap_or("0.0")
721 );
722 } else {
723 println!("{body}");
724 }
725 } else {
726 println!("No proxy running on port {port}.");
727 println!("Start with: lean-ctx proxy start");
728 }
729 }
730 _ => {
731 println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
732 }
733 }
734 return;
735 }
736 #[cfg(not(feature = "http-server"))]
737 {
738 eprintln!("lean-ctx proxy is not available in this build");
739 std::process::exit(1);
740 }
741 }
742 "init" => {
743 super::cmd_init(&rest);
744 return;
745 }
746 "setup" => {
747 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
748 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
749 let fix = rest.iter().any(|a| a == "--fix");
750 let json = rest.iter().any(|a| a == "--json");
751
752 if non_interactive || fix || json || yes {
753 let opts = setup::SetupOptions {
754 non_interactive,
755 yes,
756 fix,
757 json,
758 };
759 match setup::run_setup_with_options(opts) {
760 Ok(report) => {
761 if json {
762 println!(
763 "{}",
764 serde_json::to_string_pretty(&report)
765 .unwrap_or_else(|_| "{}".to_string())
766 );
767 }
768 if !report.success {
769 std::process::exit(1);
770 }
771 }
772 Err(e) => {
773 eprintln!("{e}");
774 std::process::exit(1);
775 }
776 }
777 } else {
778 setup::run_setup();
779 }
780 return;
781 }
782 "bootstrap" => {
783 let json = rest.iter().any(|a| a == "--json");
784 let opts = setup::SetupOptions {
785 non_interactive: true,
786 yes: true,
787 fix: true,
788 json,
789 };
790 match setup::run_setup_with_options(opts) {
791 Ok(report) => {
792 if json {
793 println!(
794 "{}",
795 serde_json::to_string_pretty(&report)
796 .unwrap_or_else(|_| "{}".to_string())
797 );
798 }
799 if !report.success {
800 std::process::exit(1);
801 }
802 }
803 Err(e) => {
804 eprintln!("{e}");
805 std::process::exit(1);
806 }
807 }
808 return;
809 }
810 "status" => {
811 let code = status::run_cli(&rest);
812 if code != 0 {
813 std::process::exit(code);
814 }
815 return;
816 }
817 "read" => {
818 super::cmd_read(&rest);
819 return;
820 }
821 "diff" => {
822 super::cmd_diff(&rest);
823 return;
824 }
825 "grep" => {
826 super::cmd_grep(&rest);
827 return;
828 }
829 "find" => {
830 super::cmd_find(&rest);
831 return;
832 }
833 "ls" => {
834 super::cmd_ls(&rest);
835 return;
836 }
837 "deps" => {
838 super::cmd_deps(&rest);
839 return;
840 }
841 "discover" => {
842 super::cmd_discover(&rest);
843 return;
844 }
845 "ghost" => {
846 super::cmd_ghost(&rest);
847 return;
848 }
849 "filter" => {
850 super::cmd_filter(&rest);
851 return;
852 }
853 "heatmap" => {
854 heatmap::cmd_heatmap(&rest);
855 return;
856 }
857 "graph" => {
858 let sub = rest.first().map_or("build", std::string::String::as_str);
859 match sub {
860 "build" => {
861 let root = rest.get(1).cloned().or_else(|| {
862 std::env::current_dir()
863 .ok()
864 .map(|p| p.to_string_lossy().to_string())
865 });
866 let root = root.unwrap_or_else(|| ".".to_string());
867 let index = core::graph_index::load_or_build(&root);
868 println!(
869 "Graph built: {} files, {} edges",
870 index.files.len(),
871 index.edges.len()
872 );
873 }
874 "export-html" => {
875 let mut root: Option<String> = None;
876 let mut out: Option<String> = None;
877 let mut max_nodes: usize = 2500;
878
879 let args = &rest[1..];
880 let mut i = 0usize;
881 while i < args.len() {
882 let a = args[i].as_str();
883 if let Some(v) = a.strip_prefix("--root=") {
884 root = Some(v.to_string());
885 } else if a == "--root" {
886 root = args.get(i + 1).cloned();
887 i += 1;
888 } else if let Some(v) = a.strip_prefix("--out=") {
889 out = Some(v.to_string());
890 } else if a == "--out" {
891 out = args.get(i + 1).cloned();
892 i += 1;
893 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
894 max_nodes = v.parse::<usize>().unwrap_or(0);
895 } else if a == "--max-nodes" {
896 let v = args.get(i + 1).map_or("", String::as_str);
897 max_nodes = v.parse::<usize>().unwrap_or(0);
898 i += 1;
899 }
900 i += 1;
901 }
902
903 let root = root
904 .or_else(|| {
905 std::env::current_dir()
906 .ok()
907 .map(|p| p.to_string_lossy().to_string())
908 })
909 .unwrap_or_else(|| ".".to_string());
910 let Some(out) = out else {
911 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
912 std::process::exit(1);
913 };
914 if max_nodes == 0 {
915 eprintln!("--max-nodes must be >= 1");
916 std::process::exit(1);
917 }
918
919 core::graph_export::export_graph_html(
920 &root,
921 std::path::Path::new(&out),
922 max_nodes,
923 )
924 .unwrap_or_else(|e| {
925 eprintln!("graph export failed: {e}");
926 std::process::exit(1);
927 });
928 println!("{out}");
929 }
930 _ => {
931 eprintln!(
932 "Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
933 );
934 std::process::exit(1);
935 }
936 }
937 return;
938 }
939 "session" => {
940 super::cmd_session();
941 return;
942 }
943 "wrapped" => {
944 super::cmd_wrapped(&rest);
945 return;
946 }
947 "sessions" => {
948 super::cmd_sessions(&rest);
949 return;
950 }
951 "benchmark" => {
952 super::cmd_benchmark(&rest);
953 return;
954 }
955 "profile" => {
956 super::cmd_profile(&rest);
957 return;
958 }
959 "config" => {
960 super::cmd_config(&rest);
961 return;
962 }
963 "stats" => {
964 super::cmd_stats(&rest);
965 return;
966 }
967 "cache" => {
968 super::cmd_cache(&rest);
969 return;
970 }
971 "theme" => {
972 super::cmd_theme(&rest);
973 return;
974 }
975 "tee" => {
976 super::cmd_tee(&rest);
977 return;
978 }
979 "terse" => {
980 super::cmd_terse(&rest);
981 return;
982 }
983 "slow-log" => {
984 super::cmd_slow_log(&rest);
985 return;
986 }
987 "update" | "--self-update" => {
988 core::updater::run(&rest);
989 return;
990 }
991 "doctor" => {
992 let code = doctor::run_cli(&rest);
993 if code != 0 {
994 std::process::exit(code);
995 }
996 return;
997 }
998 "gotchas" | "bugs" => {
999 super::cloud::cmd_gotchas(&rest);
1000 return;
1001 }
1002 "buddy" | "pet" => {
1003 super::cloud::cmd_buddy(&rest);
1004 return;
1005 }
1006 "hook" => {
1007 let action = rest.first().map_or("help", std::string::String::as_str);
1008 match action {
1009 "rewrite" => hook_handlers::handle_rewrite(),
1010 "redirect" => hook_handlers::handle_redirect(),
1011 "copilot" => hook_handlers::handle_copilot(),
1012 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1013 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1014 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1015 _ => {
1016 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1017 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1018 std::process::exit(1);
1019 }
1020 }
1021 return;
1022 }
1023 "report-issue" | "report" => {
1024 report::run(&rest);
1025 return;
1026 }
1027 "uninstall" => {
1028 let dry_run = rest.iter().any(|a| a == "--dry-run");
1029 uninstall::run(dry_run);
1030 return;
1031 }
1032 "bypass" => {
1033 if rest.is_empty() {
1034 eprintln!("Usage: lean-ctx bypass \"command\"");
1035 eprintln!("Runs the command with zero compression (raw passthrough).");
1036 std::process::exit(1);
1037 }
1038 let command = if rest.len() == 1 {
1039 rest[0].clone()
1040 } else {
1041 shell::join_command(&args[2..])
1042 };
1043 std::env::set_var("LEAN_CTX_RAW", "1");
1044 let code = shell::exec(&command);
1045 std::process::exit(code);
1046 }
1047 "safety-levels" | "safety" => {
1048 println!("{}", core::compression_safety::format_safety_table());
1049 return;
1050 }
1051 "cheat" | "cheatsheet" | "cheat-sheet" => {
1052 super::cmd_cheatsheet();
1053 return;
1054 }
1055 "login" => {
1056 super::cloud::cmd_login(&rest);
1057 return;
1058 }
1059 "register" => {
1060 super::cloud::cmd_register(&rest);
1061 return;
1062 }
1063 "forgot-password" => {
1064 super::cloud::cmd_forgot_password(&rest);
1065 return;
1066 }
1067 "sync" => {
1068 super::cloud::cmd_sync();
1069 return;
1070 }
1071 "contribute" => {
1072 super::cloud::cmd_contribute();
1073 return;
1074 }
1075 "cloud" => {
1076 super::cloud::cmd_cloud(&rest);
1077 return;
1078 }
1079 "upgrade" => {
1080 super::cloud::cmd_upgrade();
1081 return;
1082 }
1083 "--version" | "-V" => {
1084 println!("{}", core::integrity::origin_line());
1085 return;
1086 }
1087 "--help" | "-h" => {
1088 print_help();
1089 return;
1090 }
1091 "mcp" => {}
1092 _ => {
1093 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1094 print_help();
1095 std::process::exit(1);
1096 }
1097 }
1098 }
1099
1100 if let Err(e) = run_mcp_server() {
1101 tracing::error!("lean-ctx: {e}");
1102 std::process::exit(1);
1103 }
1104}
1105
1106fn passthrough(command: &str) -> ! {
1107 let (shell, flag) = shell::shell_and_flag();
1108 let status = std::process::Command::new(&shell)
1109 .arg(&flag)
1110 .arg(command)
1111 .env("LEAN_CTX_ACTIVE", "1")
1112 .status()
1113 .map_or(127, |s| s.code().unwrap_or(1));
1114 std::process::exit(status);
1115}
1116
1117fn run_async<F: std::future::Future>(future: F) -> F::Output {
1118 tokio::runtime::Runtime::new()
1119 .expect("failed to create async runtime")
1120 .block_on(future)
1121}
1122
1123fn run_mcp_server() -> Result<()> {
1124 use rmcp::ServiceExt;
1125
1126 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1127
1128 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1132 "mcp-startup",
1133 std::time::Duration::from_secs(3),
1134 std::time::Duration::from_secs(30),
1135 );
1136
1137 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1138 let worker_threads = parallelism.clamp(1, 4);
1139 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1140
1141 let rt = tokio::runtime::Builder::new_multi_thread()
1142 .worker_threads(worker_threads)
1143 .max_blocking_threads(max_blocking_threads)
1144 .enable_all()
1145 .build()?;
1146
1147 let server = tools::create_server();
1148 drop(startup_lock);
1149
1150 rt.block_on(async {
1151 core::logging::init_mcp_logging();
1152
1153 tracing::info!(
1154 "lean-ctx v{} MCP server starting",
1155 env!("CARGO_PKG_VERSION")
1156 );
1157
1158 let transport =
1159 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1160 let service = match server.serve(transport).await {
1161 Ok(s) => s,
1162 Err(e) => {
1163 let msg = e.to_string();
1164 if msg.contains("expect initialized")
1165 || msg.contains("context canceled")
1166 || msg.contains("broken pipe")
1167 {
1168 tracing::debug!("Client disconnected before init: {msg}");
1169 return Ok(());
1170 }
1171 return Err(e.into());
1172 }
1173 };
1174 match service.waiting().await {
1175 Ok(reason) => {
1176 tracing::info!("MCP server stopped: {reason:?}");
1177 }
1178 Err(e) => {
1179 let msg = e.to_string();
1180 if msg.contains("broken pipe")
1181 || msg.contains("connection reset")
1182 || msg.contains("context canceled")
1183 {
1184 tracing::info!("MCP server: transport closed ({msg})");
1185 } else {
1186 tracing::error!("MCP server error: {msg}");
1187 }
1188 }
1189 }
1190
1191 core::stats::flush();
1192 core::mode_predictor::ModePredictor::flush();
1193 core::feedback::FeedbackStore::flush();
1194
1195 Ok(())
1196 })
1197}
1198
1199fn print_help() {
1200 println!(
1201 "lean-ctx {version} — Context Runtime for AI Agents
1202
120390+ compression patterns | 49 MCP tools | Context Continuity Protocol
1204
1205USAGE:
1206 lean-ctx Start MCP server (stdio)
1207 lean-ctx serve Start MCP server (Streamable HTTP)
1208 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1209 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1210 lean-ctx -c --raw \"command\" Execute without compression (full output)
1211 lean-ctx exec \"command\" Same as -c
1212 lean-ctx shell Interactive shell with compression
1213
1214COMMANDS:
1215 gain Visual dashboard (colors, bars, sparklines, USD)
1216 gain --live Live mode: auto-refreshes every 1s in-place
1217 gain --graph 30-day savings chart
1218 gain --daily Bordered day-by-day table with USD
1219 gain --json Raw JSON export of all stats
1220 token-report [--json] Token + memory report (project + session + CEP)
1221 cep CEP impact report (score trends, cache, modes)
1222 watch Live TUI dashboard (real-time event stream)
1223 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1224 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1225 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1226 proxy status Show proxy statistics
1227 cache [list|clear|stats] Show/manage file read cache
1228 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1229 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1230 benchmark run [path] [--json] Run real benchmark on project files
1231 benchmark report [path] Generate shareable Markdown report
1232 cheatsheet Command cheat sheet & workflow quick reference
1233 setup One-command setup: shell + editor + verify
1234 bootstrap Non-interactive setup + fix (zero-config)
1235 status [--json] Show setup + MCP + rules status
1236 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1237 init --agent <name> Configure MCP for specific editor/agent
1238 read <file> [-m mode] Read file with compression
1239 diff <file1> <file2> Compressed file diff
1240 grep <pattern> [path] Search with compressed output
1241 find <pattern> [path] Find files with compressed output
1242 ls [path] Directory listing with compression
1243 deps [path] Show project dependencies
1244 discover Find uncompressed commands in shell history
1245 ghost [--json] Ghost Token report: find hidden token waste
1246 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1247 session Show adoption statistics
1248 config Show/edit configuration (~/.lean-ctx/config.toml)
1249 profile [list|show|diff|create|set] Manage context profiles
1250 theme [list|set|export|import] Customize terminal colors and themes
1251 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1252 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1253 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1254 update [--check] Self-update lean-ctx binary from GitHub Releases
1255 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1256 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1257 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1258 uninstall Remove shell hook, MCP configs, and data directory
1259
1260SHELL HOOK PATTERNS (90+):
1261 git status, log, diff, add, commit, push, pull, fetch, clone,
1262 branch, checkout, switch, merge, stash, tag, reset, remote
1263 docker build, ps, images, logs, compose, exec, network
1264 npm/pnpm install, test, run, list, outdated, audit
1265 cargo build, test, check, clippy
1266 gh pr list/view/create, issue list/view, run list/view
1267 kubectl get pods/services/deployments, logs, describe, apply
1268 python pip install/list/outdated, ruff check/format, poetry, uv
1269 linters eslint, biome, prettier, golangci-lint
1270 builds tsc, next build, vite build
1271 ruby rubocop, bundle install/update, rake test, rails test
1272 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1273 iac terraform, make, maven, gradle, dotnet, flutter, dart
1274 utils curl, grep/rg, find, ls, wget, env
1275 data JSON schema extraction, log deduplication
1276
1277READ MODES:
1278 auto Auto-select optimal mode (default)
1279 full Full content (cached re-reads = 13 tokens)
1280 map Dependency graph + API signatures
1281 signatures tree-sitter AST extraction (18 languages)
1282 task Task-relevant filtering (requires ctx_session task)
1283 reference One-line reference stub (cheap cache key)
1284 aggressive Syntax-stripped content
1285 entropy Shannon entropy filtered
1286 diff Changed lines only
1287 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1288
1289ENVIRONMENT:
1290 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1291 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1292 LEAN_CTX_RAW=1 Same as --raw for current command
1293 LEAN_CTX_AUTONOMY=false Disable autonomous features
1294 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1295
1296OPTIONS:
1297 --version, -V Show version
1298 --help, -h Show this help
1299
1300EXAMPLES:
1301 lean-ctx -c \"git status\" Compressed git output
1302 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1303 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1304 lean-ctx gain Visual terminal dashboard
1305 lean-ctx gain --live Live auto-updating terminal dashboard
1306 lean-ctx gain --graph 30-day savings chart
1307 lean-ctx gain --daily Day-by-day breakdown with USD
1308 lean-ctx token-report --json Machine-readable token + memory report
1309 lean-ctx dashboard Open web dashboard at localhost:3333
1310 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1311 lean-ctx gain --wrapped Wrapped report card (recommended)
1312 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1313 lean-ctx sessions list List all CCP sessions
1314 lean-ctx sessions show Show latest session state
1315 lean-ctx discover Find missed savings in shell history
1316 lean-ctx setup One-command setup (shell + editors + verify)
1317 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1318 lean-ctx bootstrap --json Machine-readable bootstrap report
1319 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1320 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1321 lean-ctx-off Disable all shell aliases
1322 lean-ctx-mode track Track mode: full output, stats recorded (default)
1323 lean-ctx-mode compress Compress mode: all output compressed (power users)
1324 lean-ctx-mode off Same as lean-ctx-off
1325 lean-ctx-status Show whether compression is active
1326 lean-ctx init --agent pi Install Pi Coding Agent extension
1327 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1328 lean-ctx doctor --fix --json Repair + machine-readable report
1329 lean-ctx status --json Machine-readable current status
1330 lean-ctx read src/main.rs -m map
1331 lean-ctx grep \"pub fn\" src/
1332 lean-ctx deps .
1333
1334CLOUD:
1335 cloud status Show cloud connection status
1336 login <email> Log into existing LeanCTX Cloud account
1337 register <email> Create a new LeanCTX Cloud account
1338 forgot-password <email> Send password reset email
1339 sync Upload local stats to cloud dashboard
1340 contribute Share anonymized compression data
1341
1342TROUBLESHOOTING:
1343 Commands broken? lean-ctx-off (fixes current session)
1344 Permanent fix? lean-ctx uninstall (removes all hooks)
1345 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1346 Binary missing? Aliases auto-fallback to original commands (safe)
1347 Preview init? lean-ctx init --global --dry-run
1348
1349WEBSITE: https://leanctx.com
1350GITHUB: https://github.com/yvgude/lean-ctx
1351",
1352 version = env!("CARGO_PKG_VERSION"),
1353 );
1354}