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 "instructions" => {
209 crate::cli::cmd_instructions(&rest);
210 return;
211 }
212 "index" => {
213 crate::cli::cmd_index(&rest);
214 return;
215 }
216 "cep" => {
217 println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
218 return;
219 }
220 "dashboard" => {
221 if rest.iter().any(|a| a == "--help" || a == "-h") {
222 println!("Usage: lean-ctx dashboard [--port=N] [--host=H] [--project=PATH]");
223 println!("Examples:");
224 println!(" lean-ctx dashboard");
225 println!(" lean-ctx dashboard --port=3333");
226 println!(" lean-ctx dashboard --host=0.0.0.0");
227 return;
228 }
229 let port = rest
230 .iter()
231 .find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
232 .and_then(|p| p.parse().ok());
233 let host = rest
234 .iter()
235 .find_map(|p| p.strip_prefix("--host=").or_else(|| p.strip_prefix("-H=")))
236 .map(String::from);
237 let project = rest
238 .iter()
239 .find_map(|p| p.strip_prefix("--project="))
240 .map(String::from);
241 if let Some(ref p) = project {
242 std::env::set_var("LEAN_CTX_DASHBOARD_PROJECT", p);
243 }
244 run_async(dashboard::start(port, host));
245 return;
246 }
247 "team" => {
248 let sub = rest.first().map_or("help", std::string::String::as_str);
249 match sub {
250 "serve" => {
251 #[cfg(feature = "team-server")]
252 {
253 let cfg_path = rest
254 .iter()
255 .enumerate()
256 .find_map(|(i, a)| {
257 if let Some(v) = a.strip_prefix("--config=") {
258 return Some(v.to_string());
259 }
260 if a == "--config" {
261 return rest.get(i + 1).cloned();
262 }
263 None
264 })
265 .unwrap_or_default();
266
267 if cfg_path.trim().is_empty() {
268 eprintln!("Usage: lean-ctx team serve --config <path>");
269 std::process::exit(1);
270 }
271
272 let cfg = crate::http_server::team::TeamServerConfig::load(
273 std::path::Path::new(&cfg_path),
274 )
275 .unwrap_or_else(|e| {
276 eprintln!("Invalid team config: {e}");
277 std::process::exit(1);
278 });
279
280 if let Err(e) = run_async(crate::http_server::team::serve_team(cfg)) {
281 tracing::error!("Team server error: {e}");
282 std::process::exit(1);
283 }
284 return;
285 }
286 #[cfg(not(feature = "team-server"))]
287 {
288 eprintln!("lean-ctx team serve is not available in this build");
289 std::process::exit(1);
290 }
291 }
292 "token" => {
293 let action = rest.get(1).map_or("help", std::string::String::as_str);
294 if action == "create" {
295 #[cfg(feature = "team-server")]
296 {
297 let args = &rest[2..];
298 let cfg_path = args
299 .iter()
300 .enumerate()
301 .find_map(|(i, a)| {
302 if let Some(v) = a.strip_prefix("--config=") {
303 return Some(v.to_string());
304 }
305 if a == "--config" {
306 return args.get(i + 1).cloned();
307 }
308 None
309 })
310 .unwrap_or_default();
311 let token_id = args
312 .iter()
313 .enumerate()
314 .find_map(|(i, a)| {
315 if let Some(v) = a.strip_prefix("--id=") {
316 return Some(v.to_string());
317 }
318 if a == "--id" {
319 return args.get(i + 1).cloned();
320 }
321 None
322 })
323 .unwrap_or_default();
324 let scopes_csv = args
325 .iter()
326 .enumerate()
327 .find_map(|(i, a)| {
328 if let Some(v) = a.strip_prefix("--scopes=") {
329 return Some(v.to_string());
330 }
331 if let Some(v) = a.strip_prefix("--scope=") {
332 return Some(v.to_string());
333 }
334 if a == "--scopes" || a == "--scope" {
335 return args.get(i + 1).cloned();
336 }
337 None
338 })
339 .unwrap_or_default();
340
341 if cfg_path.trim().is_empty()
342 || token_id.trim().is_empty()
343 || scopes_csv.trim().is_empty()
344 {
345 eprintln!(
346 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
347 );
348 std::process::exit(1);
349 }
350
351 let cfg_p = std::path::PathBuf::from(&cfg_path);
352 let mut cfg = crate::http_server::team::TeamServerConfig::load(
353 cfg_p.as_path(),
354 )
355 .unwrap_or_else(|e| {
356 eprintln!("Invalid team config: {e}");
357 std::process::exit(1);
358 });
359
360 let mut scopes = Vec::new();
361 for part in scopes_csv.split(',') {
362 let p = part.trim().to_ascii_lowercase();
363 if p.is_empty() {
364 continue;
365 }
366 let scope = match p.as_str() {
367 "search" => crate::http_server::team::TeamScope::Search,
368 "graph" => crate::http_server::team::TeamScope::Graph,
369 "artifacts" => {
370 crate::http_server::team::TeamScope::Artifacts
371 }
372 "index" => crate::http_server::team::TeamScope::Index,
373 "events" => crate::http_server::team::TeamScope::Events,
374 "sessionmutations" | "session_mutations" => {
375 crate::http_server::team::TeamScope::SessionMutations
376 }
377 "knowledge" => {
378 crate::http_server::team::TeamScope::Knowledge
379 }
380 "audit" => crate::http_server::team::TeamScope::Audit,
381 _ => {
382 eprintln!("Unknown scope: {p}. Valid: search, graph, artifacts, index, events, sessionmutations, knowledge, audit");
383 std::process::exit(1);
384 }
385 };
386 if !scopes.contains(&scope) {
387 scopes.push(scope);
388 }
389 }
390 if scopes.is_empty() {
391 eprintln!("At least 1 scope is required");
392 std::process::exit(1);
393 }
394
395 let (token, hash) = crate::http_server::team::create_token()
396 .unwrap_or_else(|e| {
397 eprintln!("Token generation failed: {e}");
398 std::process::exit(1);
399 });
400
401 cfg.tokens.push(crate::http_server::team::TeamTokenConfig {
402 id: token_id,
403 sha256_hex: hash,
404 scopes,
405 });
406
407 cfg.save(cfg_p.as_path()).unwrap_or_else(|e| {
408 eprintln!("Failed to write config: {e}");
409 std::process::exit(1);
410 });
411
412 println!("{token}");
413 return;
414 }
415
416 #[cfg(not(feature = "team-server"))]
417 {
418 eprintln!("lean-ctx team token is not available in this build");
419 std::process::exit(1);
420 }
421 }
422 eprintln!(
423 "Usage: lean-ctx team token create --config <path> --id <id> --scopes <csv>"
424 );
425 std::process::exit(1);
426 }
427 "sync" => {
428 #[cfg(feature = "team-server")]
429 {
430 let args = &rest[1..];
431 let cfg_path = args
432 .iter()
433 .enumerate()
434 .find_map(|(i, a)| {
435 if let Some(v) = a.strip_prefix("--config=") {
436 return Some(v.to_string());
437 }
438 if a == "--config" {
439 return args.get(i + 1).cloned();
440 }
441 None
442 })
443 .unwrap_or_default();
444 if cfg_path.trim().is_empty() {
445 eprintln!(
446 "Usage: lean-ctx team sync --config <path> [--workspace <id>]"
447 );
448 std::process::exit(1);
449 }
450 let only_ws = args.iter().enumerate().find_map(|(i, a)| {
451 if let Some(v) = a.strip_prefix("--workspace=") {
452 return Some(v.to_string());
453 }
454 if let Some(v) = a.strip_prefix("--workspace-id=") {
455 return Some(v.to_string());
456 }
457 if a == "--workspace" || a == "--workspace-id" {
458 return args.get(i + 1).cloned();
459 }
460 None
461 });
462
463 let cfg = crate::http_server::team::TeamServerConfig::load(
464 std::path::Path::new(&cfg_path),
465 )
466 .unwrap_or_else(|e| {
467 eprintln!("Invalid team config: {e}");
468 std::process::exit(1);
469 });
470
471 for ws in &cfg.workspaces {
472 if let Some(ref only) = only_ws {
473 if ws.id != *only {
474 continue;
475 }
476 }
477 let git_dir = ws.root.join(".git");
478 if !git_dir.exists() {
479 eprintln!(
480 "workspace '{}' root is not a git repo: {}",
481 ws.id,
482 ws.root.display()
483 );
484 std::process::exit(1);
485 }
486 let status = std::process::Command::new("git")
487 .arg("-C")
488 .arg(&ws.root)
489 .args(["fetch", "--all", "--prune"])
490 .status()
491 .unwrap_or_else(|e| {
492 eprintln!(
493 "git fetch failed for workspace '{}': {e}",
494 ws.id
495 );
496 std::process::exit(1);
497 });
498 if !status.success() {
499 eprintln!(
500 "git fetch failed for workspace '{}' (exit={})",
501 ws.id,
502 status.code().unwrap_or(1)
503 );
504 std::process::exit(1);
505 }
506 }
507 return;
508 }
509 #[cfg(not(feature = "team-server"))]
510 {
511 eprintln!("lean-ctx team sync is not available in this build");
512 std::process::exit(1);
513 }
514 }
515 _ => {
516 eprintln!(
517 "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>]"
518 );
519 std::process::exit(1);
520 }
521 }
522 }
523 "serve" => {
524 #[cfg(feature = "http-server")]
525 {
526 let mut cfg = crate::http_server::HttpServerConfig::default();
527 let mut daemon_mode = false;
528 let mut stop_mode = false;
529 let mut status_mode = false;
530 let mut foreground_daemon = false;
531 let mut i = 0;
532 while i < rest.len() {
533 match rest[i].as_str() {
534 "--daemon" | "-d" => daemon_mode = true,
535 "--stop" => stop_mode = true,
536 "--status" => status_mode = true,
537 "--_foreground-daemon" => foreground_daemon = true,
538 "--host" | "-H" => {
539 i += 1;
540 if i < rest.len() {
541 cfg.host.clone_from(&rest[i]);
542 }
543 }
544 arg if arg.starts_with("--host=") => {
545 cfg.host = arg["--host=".len()..].to_string();
546 }
547 "--port" | "-p" => {
548 i += 1;
549 if i < rest.len() {
550 if let Ok(p) = rest[i].parse::<u16>() {
551 cfg.port = p;
552 }
553 }
554 }
555 arg if arg.starts_with("--port=") => {
556 if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
557 cfg.port = p;
558 }
559 }
560 "--project-root" => {
561 i += 1;
562 if i < rest.len() {
563 cfg.project_root = std::path::PathBuf::from(&rest[i]);
564 }
565 }
566 arg if arg.starts_with("--project-root=") => {
567 cfg.project_root =
568 std::path::PathBuf::from(&arg["--project-root=".len()..]);
569 }
570 "--auth-token" => {
571 i += 1;
572 if i < rest.len() {
573 cfg.auth_token = Some(rest[i].clone());
574 }
575 }
576 arg if arg.starts_with("--auth-token=") => {
577 cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
578 }
579 "--stateful" => cfg.stateful_mode = true,
580 "--stateless" => cfg.stateful_mode = false,
581 "--json" => cfg.json_response = true,
582 "--sse" => cfg.json_response = false,
583 "--disable-host-check" => cfg.disable_host_check = true,
584 "--allowed-host" => {
585 i += 1;
586 if i < rest.len() {
587 cfg.allowed_hosts.push(rest[i].clone());
588 }
589 }
590 arg if arg.starts_with("--allowed-host=") => {
591 cfg.allowed_hosts
592 .push(arg["--allowed-host=".len()..].to_string());
593 }
594 "--max-body-bytes" => {
595 i += 1;
596 if i < rest.len() {
597 if let Ok(n) = rest[i].parse::<usize>() {
598 cfg.max_body_bytes = n;
599 }
600 }
601 }
602 arg if arg.starts_with("--max-body-bytes=") => {
603 if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
604 cfg.max_body_bytes = n;
605 }
606 }
607 "--max-concurrency" => {
608 i += 1;
609 if i < rest.len() {
610 if let Ok(n) = rest[i].parse::<usize>() {
611 cfg.max_concurrency = n;
612 }
613 }
614 }
615 arg if arg.starts_with("--max-concurrency=") => {
616 if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
617 cfg.max_concurrency = n;
618 }
619 }
620 "--max-rps" => {
621 i += 1;
622 if i < rest.len() {
623 if let Ok(n) = rest[i].parse::<u32>() {
624 cfg.max_rps = n;
625 }
626 }
627 }
628 arg if arg.starts_with("--max-rps=") => {
629 if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
630 cfg.max_rps = n;
631 }
632 }
633 "--rate-burst" => {
634 i += 1;
635 if i < rest.len() {
636 if let Ok(n) = rest[i].parse::<u32>() {
637 cfg.rate_burst = n;
638 }
639 }
640 }
641 arg if arg.starts_with("--rate-burst=") => {
642 if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
643 cfg.rate_burst = n;
644 }
645 }
646 "--request-timeout-ms" => {
647 i += 1;
648 if i < rest.len() {
649 if let Ok(n) = rest[i].parse::<u64>() {
650 cfg.request_timeout_ms = n;
651 }
652 }
653 }
654 arg if arg.starts_with("--request-timeout-ms=") => {
655 if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
656 cfg.request_timeout_ms = n;
657 }
658 }
659 "--help" | "-h" => {
660 eprintln!(
661 "Usage: lean-ctx serve [--host H] [--port N] [--project-root DIR] [--daemon] [--stop] [--status]\\n\\
662 \\n\\
663 Options:\\n\\
664 --daemon, -d Start as background daemon (UDS)\\n\\
665 --stop Stop running daemon\\n\\
666 --status Show daemon status\\n\\
667 --host, -H Bind host (default: 127.0.0.1)\\n\\
668 --port, -p Bind port (default: 8080)\\n\\
669 --project-root Resolve relative paths against this root (default: cwd)\\n\\
670 --auth-token Require Authorization: Bearer <token> (required for non-loopback binds)\\n\\
671 --stateful/--stateless Streamable HTTP session mode (default: stateless)\\n\\
672 --json/--sse Response framing in stateless mode (default: json)\\n\\
673 --max-body-bytes Max request body size in bytes (default: 2097152)\\n\\
674 --max-concurrency Max concurrent requests (default: 32)\\n\\
675 --max-rps Max requests/sec (global, default: 50)\\n\\
676 --rate-burst Rate limiter burst (global, default: 100)\\n\\
677 --request-timeout-ms REST tool-call timeout (default: 30000)\\n\\
678 --allowed-host Add allowed Host header (repeatable)\\n\\
679 --disable-host-check Disable Host header validation (unsafe)"
680 );
681 return;
682 }
683 _ => {}
684 }
685 i += 1;
686 }
687
688 if stop_mode {
689 if let Err(e) = crate::daemon::stop_daemon() {
690 eprintln!("Error: {e}");
691 std::process::exit(1);
692 }
693 return;
694 }
695
696 if status_mode {
697 println!("{}", crate::daemon::daemon_status());
698 return;
699 }
700
701 if daemon_mode {
702 if let Err(e) = crate::daemon::start_daemon(&rest) {
703 eprintln!("Error: {e}");
704 std::process::exit(1);
705 }
706 return;
707 }
708
709 if foreground_daemon {
710 if let Err(e) = crate::daemon::init_foreground_daemon() {
711 eprintln!("Error writing PID file: {e}");
712 std::process::exit(1);
713 }
714 let addr = crate::daemon::daemon_addr();
715 if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
716 {
717 tracing::error!("Daemon server error: {e}");
718 crate::daemon::cleanup_daemon_files();
719 std::process::exit(1);
720 }
721 crate::daemon::cleanup_daemon_files();
722 return;
723 }
724
725 if cfg.auth_token.is_none() {
726 if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
727 if !v.trim().is_empty() {
728 cfg.auth_token = Some(v);
729 }
730 }
731 }
732
733 if let Err(e) = run_async(crate::http_server::serve(cfg)) {
734 tracing::error!("HTTP server error: {e}");
735 std::process::exit(1);
736 }
737 return;
738 }
739 #[cfg(not(feature = "http-server"))]
740 {
741 eprintln!("lean-ctx serve is not available in this build");
742 std::process::exit(1);
743 }
744 }
745 "watch" => {
746 if rest.iter().any(|a| a == "--help" || a == "-h") {
747 println!("Usage: lean-ctx watch");
748 println!(" Live TUI dashboard (real-time event stream).");
749 return;
750 }
751 if let Err(e) = tui::run() {
752 tracing::error!("TUI error: {e}");
753 std::process::exit(1);
754 }
755 return;
756 }
757 "proxy" => {
758 #[cfg(feature = "http-server")]
759 {
760 let sub = rest.first().map_or("help", std::string::String::as_str);
761 match sub {
762 "start" => {
763 let port: u16 = rest
764 .iter()
765 .find_map(|p| {
766 p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
767 })
768 .and_then(|p| p.parse().ok())
769 .unwrap_or(4444);
770 let autostart = rest.iter().any(|a| a == "--autostart");
771 if autostart {
772 crate::proxy_autostart::install(port, false);
773 return;
774 }
775 if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
776 tracing::error!("Proxy error: {e}");
777 std::process::exit(1);
778 }
779 }
780 "stop" => {
781 match ureq::get(&format!(
782 "http://127.0.0.1:{}/health",
783 rest.iter()
784 .find_map(|p| p.strip_prefix("--port="))
785 .and_then(|p| p.parse::<u16>().ok())
786 .unwrap_or(4444)
787 ))
788 .call()
789 {
790 Ok(_) => {
791 println!("Proxy is running. Use Ctrl+C or kill the process.");
792 }
793 Err(_) => {
794 println!("No proxy running on that port.");
795 }
796 }
797 }
798 "status" => {
799 let port: u16 = rest
800 .iter()
801 .find_map(|p| p.strip_prefix("--port="))
802 .and_then(|p| p.parse().ok())
803 .unwrap_or(4444);
804 if let Ok(resp) =
805 ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
806 {
807 let body = resp.into_body().read_to_string().unwrap_or_default();
808 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
809 println!("lean-ctx proxy status:");
810 println!(" Requests: {}", v["requests_total"]);
811 println!(" Compressed: {}", v["requests_compressed"]);
812 println!(" Tokens saved: {}", v["tokens_saved"]);
813 println!(
814 " Compression: {}%",
815 v["compression_ratio_pct"].as_str().unwrap_or("0.0")
816 );
817 } else {
818 println!("{body}");
819 }
820 } else {
821 println!("No proxy running on port {port}.");
822 println!("Start with: lean-ctx proxy start");
823 }
824 }
825 _ => {
826 println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
827 }
828 }
829 return;
830 }
831 #[cfg(not(feature = "http-server"))]
832 {
833 eprintln!("lean-ctx proxy is not available in this build");
834 std::process::exit(1);
835 }
836 }
837 "init" => {
838 super::cmd_init(&rest);
839 return;
840 }
841 "setup" => {
842 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
843 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
844 let fix = rest.iter().any(|a| a == "--fix");
845 let json = rest.iter().any(|a| a == "--json");
846 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
847
848 if non_interactive || fix || json || yes {
849 let opts = setup::SetupOptions {
850 non_interactive,
851 yes,
852 fix,
853 json,
854 no_auto_approve,
855 };
856 match setup::run_setup_with_options(opts) {
857 Ok(report) => {
858 if json {
859 println!(
860 "{}",
861 serde_json::to_string_pretty(&report)
862 .unwrap_or_else(|_| "{}".to_string())
863 );
864 }
865 if !report.success {
866 std::process::exit(1);
867 }
868 }
869 Err(e) => {
870 eprintln!("{e}");
871 std::process::exit(1);
872 }
873 }
874 } else {
875 setup::run_setup();
876 }
877 return;
878 }
879 "install" => {
880 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
881 let json = rest.iter().any(|a| a == "--json");
882 if !repair {
883 eprintln!("Usage: lean-ctx install --repair [--json]");
884 std::process::exit(1);
885 }
886 let opts = setup::SetupOptions {
887 non_interactive: true,
888 yes: true,
889 fix: true,
890 json,
891 ..Default::default()
892 };
893 match setup::run_setup_with_options(opts) {
894 Ok(report) => {
895 if json {
896 println!(
897 "{}",
898 serde_json::to_string_pretty(&report)
899 .unwrap_or_else(|_| "{}".to_string())
900 );
901 }
902 if !report.success {
903 std::process::exit(1);
904 }
905 }
906 Err(e) => {
907 eprintln!("{e}");
908 std::process::exit(1);
909 }
910 }
911 return;
912 }
913 "bootstrap" => {
914 let json = rest.iter().any(|a| a == "--json");
915 let opts = setup::SetupOptions {
916 non_interactive: true,
917 yes: true,
918 fix: true,
919 json,
920 ..Default::default()
921 };
922 match setup::run_setup_with_options(opts) {
923 Ok(report) => {
924 if json {
925 println!(
926 "{}",
927 serde_json::to_string_pretty(&report)
928 .unwrap_or_else(|_| "{}".to_string())
929 );
930 }
931 if !report.success {
932 std::process::exit(1);
933 }
934 }
935 Err(e) => {
936 eprintln!("{e}");
937 std::process::exit(1);
938 }
939 }
940 return;
941 }
942 "status" => {
943 let code = status::run_cli(&rest);
944 if code != 0 {
945 std::process::exit(code);
946 }
947 return;
948 }
949 "read" => {
950 super::cmd_read(&rest);
951 core::stats::flush();
952 return;
953 }
954 "diff" => {
955 super::cmd_diff(&rest);
956 core::stats::flush();
957 return;
958 }
959 "grep" => {
960 super::cmd_grep(&rest);
961 core::stats::flush();
962 return;
963 }
964 "find" => {
965 super::cmd_find(&rest);
966 core::stats::flush();
967 return;
968 }
969 "ls" => {
970 super::cmd_ls(&rest);
971 core::stats::flush();
972 return;
973 }
974 "deps" => {
975 super::cmd_deps(&rest);
976 core::stats::flush();
977 return;
978 }
979 "discover" => {
980 super::cmd_discover(&rest);
981 return;
982 }
983 "ghost" => {
984 super::cmd_ghost(&rest);
985 return;
986 }
987 "filter" => {
988 super::cmd_filter(&rest);
989 return;
990 }
991 "heatmap" => {
992 heatmap::cmd_heatmap(&rest);
993 return;
994 }
995 "graph" => {
996 let sub = rest.first().map_or("build", std::string::String::as_str);
997 match sub {
998 "build" => {
999 let root = rest.get(1).cloned().or_else(|| {
1000 std::env::current_dir()
1001 .ok()
1002 .map(|p| p.to_string_lossy().to_string())
1003 });
1004 let root = root.unwrap_or_else(|| ".".to_string());
1005 let index = core::graph_index::load_or_build(&root);
1006 println!(
1007 "Graph built: {} files, {} edges",
1008 index.files.len(),
1009 index.edges.len()
1010 );
1011 }
1012 "export-html" => {
1013 let mut root: Option<String> = None;
1014 let mut out: Option<String> = None;
1015 let mut max_nodes: usize = 2500;
1016
1017 let args = &rest[1..];
1018 let mut i = 0usize;
1019 while i < args.len() {
1020 let a = args[i].as_str();
1021 if let Some(v) = a.strip_prefix("--root=") {
1022 root = Some(v.to_string());
1023 } else if a == "--root" {
1024 root = args.get(i + 1).cloned();
1025 i += 1;
1026 } else if let Some(v) = a.strip_prefix("--out=") {
1027 out = Some(v.to_string());
1028 } else if a == "--out" {
1029 out = args.get(i + 1).cloned();
1030 i += 1;
1031 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1032 max_nodes = v.parse::<usize>().unwrap_or(0);
1033 } else if a == "--max-nodes" {
1034 let v = args.get(i + 1).map_or("", String::as_str);
1035 max_nodes = v.parse::<usize>().unwrap_or(0);
1036 i += 1;
1037 }
1038 i += 1;
1039 }
1040
1041 let root = root
1042 .or_else(|| {
1043 std::env::current_dir()
1044 .ok()
1045 .map(|p| p.to_string_lossy().to_string())
1046 })
1047 .unwrap_or_else(|| ".".to_string());
1048 let Some(out) = out else {
1049 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1050 std::process::exit(1);
1051 };
1052 if max_nodes == 0 {
1053 eprintln!("--max-nodes must be >= 1");
1054 std::process::exit(1);
1055 }
1056
1057 core::graph_export::export_graph_html(
1058 &root,
1059 std::path::Path::new(&out),
1060 max_nodes,
1061 )
1062 .unwrap_or_else(|e| {
1063 eprintln!("graph export failed: {e}");
1064 std::process::exit(1);
1065 });
1066 println!("{out}");
1067 }
1068 _ => {
1069 eprintln!(
1070 "Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1071 );
1072 std::process::exit(1);
1073 }
1074 }
1075 return;
1076 }
1077 "smells" => {
1078 let action = rest.first().map_or("summary", String::as_str);
1079 let rule = rest.iter().enumerate().find_map(|(i, a)| {
1080 if let Some(v) = a.strip_prefix("--rule=") {
1081 return Some(v.to_string());
1082 }
1083 if a == "--rule" {
1084 return rest.get(i + 1).cloned();
1085 }
1086 None
1087 });
1088 let path = rest.iter().enumerate().find_map(|(i, a)| {
1089 if let Some(v) = a.strip_prefix("--path=") {
1090 return Some(v.to_string());
1091 }
1092 if a == "--path" {
1093 return rest.get(i + 1).cloned();
1094 }
1095 None
1096 });
1097 let root = rest
1098 .iter()
1099 .enumerate()
1100 .find_map(|(i, a)| {
1101 if let Some(v) = a.strip_prefix("--root=") {
1102 return Some(v.to_string());
1103 }
1104 if a == "--root" {
1105 return rest.get(i + 1).cloned();
1106 }
1107 None
1108 })
1109 .or_else(|| {
1110 std::env::current_dir()
1111 .ok()
1112 .map(|p| p.to_string_lossy().to_string())
1113 })
1114 .unwrap_or_else(|| ".".to_string());
1115 let fmt = if rest.iter().any(|a| a == "--json") {
1116 Some("json")
1117 } else {
1118 None
1119 };
1120 println!(
1121 "{}",
1122 tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1123 );
1124 return;
1125 }
1126 "session" => {
1127 super::cmd_session_action(&rest);
1128 return;
1129 }
1130 "control" | "context-control" => {
1131 super::cmd_control(&rest);
1132 return;
1133 }
1134 "plan" | "context-plan" => {
1135 super::cmd_plan(&rest);
1136 return;
1137 }
1138 "compile" | "context-compile" => {
1139 super::cmd_compile(&rest);
1140 return;
1141 }
1142 "knowledge" => {
1143 super::cmd_knowledge(&rest);
1144 return;
1145 }
1146 "overview" => {
1147 super::cmd_overview(&rest);
1148 return;
1149 }
1150 "compress" => {
1151 super::cmd_compress(&rest);
1152 return;
1153 }
1154 "wrapped" => {
1155 super::cmd_wrapped(&rest);
1156 return;
1157 }
1158 "sessions" => {
1159 super::cmd_sessions(&rest);
1160 return;
1161 }
1162 "benchmark" => {
1163 super::cmd_benchmark(&rest);
1164 return;
1165 }
1166 "profile" => {
1167 super::cmd_profile(&rest);
1168 return;
1169 }
1170 "config" => {
1171 super::cmd_config(&rest);
1172 return;
1173 }
1174 "stats" => {
1175 super::cmd_stats(&rest);
1176 return;
1177 }
1178 "cache" => {
1179 super::cmd_cache(&rest);
1180 return;
1181 }
1182 "theme" => {
1183 super::cmd_theme(&rest);
1184 return;
1185 }
1186 "tee" => {
1187 super::cmd_tee(&rest);
1188 return;
1189 }
1190 "terse" | "compression" => {
1191 super::cmd_compression(&rest);
1192 return;
1193 }
1194 "slow-log" => {
1195 super::cmd_slow_log(&rest);
1196 return;
1197 }
1198 "update" | "--self-update" => {
1199 core::updater::run(&rest);
1200 return;
1201 }
1202 "doctor" => {
1203 let code = doctor::run_cli(&rest);
1204 if code != 0 {
1205 std::process::exit(code);
1206 }
1207 return;
1208 }
1209 "gotchas" | "bugs" => {
1210 super::cloud::cmd_gotchas(&rest);
1211 return;
1212 }
1213 "learn" => {
1214 super::cmd_learn(&rest);
1215 return;
1216 }
1217 "buddy" | "pet" => {
1218 super::cloud::cmd_buddy(&rest);
1219 return;
1220 }
1221 "hook" => {
1222 hook_handlers::mark_hook_environment();
1223 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1224 let action = rest.first().map_or("help", std::string::String::as_str);
1225 match action {
1226 "rewrite" => hook_handlers::handle_rewrite(),
1227 "redirect" => hook_handlers::handle_redirect(),
1228 "observe" => hook_handlers::handle_observe(),
1229 "copilot" => hook_handlers::handle_copilot(),
1230 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1231 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1232 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1233 _ => {
1234 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1235 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1236 std::process::exit(1);
1237 }
1238 }
1239 return;
1240 }
1241 "report-issue" | "report" => {
1242 report::run(&rest);
1243 return;
1244 }
1245 "uninstall" => {
1246 let dry_run = rest.iter().any(|a| a == "--dry-run");
1247 uninstall::run(dry_run);
1248 return;
1249 }
1250 "bypass" => {
1251 if rest.is_empty() {
1252 eprintln!("Usage: lean-ctx bypass \"command\"");
1253 eprintln!("Runs the command with zero compression (raw passthrough).");
1254 std::process::exit(1);
1255 }
1256 let command = if rest.len() == 1 {
1257 rest[0].clone()
1258 } else {
1259 shell::join_command(&args[2..])
1260 };
1261 std::env::set_var("LEAN_CTX_RAW", "1");
1262 let code = shell::exec(&command);
1263 std::process::exit(code);
1264 }
1265 "safety-levels" | "safety" => {
1266 println!("{}", core::compression_safety::format_safety_table());
1267 return;
1268 }
1269 "cheat" | "cheatsheet" | "cheat-sheet" => {
1270 super::cmd_cheatsheet();
1271 return;
1272 }
1273 "login" => {
1274 super::cloud::cmd_login(&rest);
1275 return;
1276 }
1277 "register" => {
1278 super::cloud::cmd_register(&rest);
1279 return;
1280 }
1281 "forgot-password" => {
1282 super::cloud::cmd_forgot_password(&rest);
1283 return;
1284 }
1285 "sync" => {
1286 super::cloud::cmd_sync();
1287 return;
1288 }
1289 "contribute" => {
1290 super::cloud::cmd_contribute();
1291 return;
1292 }
1293 "cloud" => {
1294 super::cloud::cmd_cloud(&rest);
1295 return;
1296 }
1297 "upgrade" => {
1298 super::cloud::cmd_upgrade();
1299 return;
1300 }
1301 "--version" | "-V" => {
1302 println!("{}", core::integrity::origin_line());
1303 return;
1304 }
1305 "--help" | "-h" => {
1306 print_help();
1307 return;
1308 }
1309 "mcp" => {}
1310 _ => {
1311 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1312 print_help();
1313 std::process::exit(1);
1314 }
1315 }
1316 }
1317
1318 if let Err(e) = run_mcp_server() {
1319 tracing::error!("lean-ctx: {e}");
1320 std::process::exit(1);
1321 }
1322}
1323
1324fn passthrough(command: &str) -> ! {
1325 let (shell, flag) = shell::shell_and_flag();
1326 let status = std::process::Command::new(&shell)
1327 .arg(&flag)
1328 .arg(command)
1329 .env("LEAN_CTX_ACTIVE", "1")
1330 .status()
1331 .map_or(127, |s| s.code().unwrap_or(1));
1332 std::process::exit(status);
1333}
1334
1335fn run_async<F: std::future::Future>(future: F) -> F::Output {
1336 tokio::runtime::Runtime::new()
1337 .expect("failed to create async runtime")
1338 .block_on(future)
1339}
1340
1341fn run_mcp_server() -> Result<()> {
1342 use rmcp::ServiceExt;
1343
1344 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1345
1346 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1347
1348 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1352 "mcp-startup",
1353 std::time::Duration::from_secs(3),
1354 std::time::Duration::from_secs(30),
1355 );
1356
1357 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1358 let worker_threads = parallelism.clamp(1, 4);
1359 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1360
1361 let rt = tokio::runtime::Builder::new_multi_thread()
1362 .worker_threads(worker_threads)
1363 .max_blocking_threads(max_blocking_threads)
1364 .enable_all()
1365 .build()?;
1366
1367 let server = tools::create_server();
1368 drop(startup_lock);
1369
1370 rt.block_on(async {
1371 core::logging::init_mcp_logging();
1372 core::protocol::set_mcp_context(true);
1373
1374 tracing::info!(
1375 "lean-ctx v{} MCP server starting",
1376 env!("CARGO_PKG_VERSION")
1377 );
1378
1379 let transport =
1380 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1381 let server_handle = server.clone();
1382 let service = match server.serve(transport).await {
1383 Ok(s) => s,
1384 Err(e) => {
1385 let msg = e.to_string();
1386 if msg.contains("expect initialized")
1387 || msg.contains("context canceled")
1388 || msg.contains("broken pipe")
1389 {
1390 tracing::debug!("Client disconnected before init: {msg}");
1391 return Ok(());
1392 }
1393 return Err(e.into());
1394 }
1395 };
1396 match service.waiting().await {
1397 Ok(reason) => {
1398 tracing::info!("MCP server stopped: {reason:?}");
1399 }
1400 Err(e) => {
1401 let msg = e.to_string();
1402 if msg.contains("broken pipe")
1403 || msg.contains("connection reset")
1404 || msg.contains("context canceled")
1405 {
1406 tracing::info!("MCP server: transport closed ({msg})");
1407 } else {
1408 tracing::error!("MCP server error: {msg}");
1409 }
1410 }
1411 }
1412
1413 server_handle.shutdown().await;
1414
1415 core::stats::flush();
1416 core::heatmap::flush();
1417 core::mode_predictor::ModePredictor::flush();
1418 core::feedback::FeedbackStore::flush();
1419
1420 Ok(())
1421 })
1422}
1423
1424fn print_help() {
1425 println!(
1426 "lean-ctx {version} — Context Runtime for AI Agents
1427
142895+ compression patterns | 59 MCP tools | Context Continuity Protocol
1429
1430USAGE:
1431 lean-ctx Start MCP server (stdio)
1432 lean-ctx serve Start MCP server (Streamable HTTP)
1433 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1434 lean-ctx serve --stop Stop running daemon
1435 lean-ctx serve --status Show daemon status
1436 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1437 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1438 lean-ctx -c --raw \"command\" Execute without compression (full output)
1439 lean-ctx exec \"command\" Same as -c
1440 lean-ctx shell Interactive shell with compression
1441
1442COMMANDS:
1443 gain Visual dashboard (colors, bars, sparklines, USD)
1444 gain --live Live mode: auto-refreshes every 1s in-place
1445 gain --graph 30-day savings chart
1446 gain --daily Bordered day-by-day table with USD
1447 gain --json Raw JSON export of all stats
1448 token-report [--json] Token + memory report (project + session + CEP)
1449 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1450 index <status|build|build-full|watch> Codebase index utilities
1451 cep CEP impact report (score trends, cache, modes)
1452 watch Live TUI dashboard (real-time event stream)
1453 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1454 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1455 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1456 proxy status Show proxy statistics
1457 cache [list|clear|stats] Show/manage file read cache
1458 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1459 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1460 benchmark run [path] [--json] Run real benchmark on project files
1461 benchmark report [path] Generate shareable Markdown report
1462 cheatsheet Command cheat sheet & workflow quick reference
1463 setup One-command setup: shell + editor + verify
1464 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1465 bootstrap Non-interactive setup + fix (zero-config)
1466 status [--json] Show setup + MCP + rules status
1467 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1468 init --agent <name> Configure MCP for specific editor/agent
1469 read <file> [-m mode] Read file with compression
1470 diff <file1> <file2> Compressed file diff
1471 grep <pattern> [path] Search with compressed output
1472 find <pattern> [path] Find files with compressed output
1473 ls [path] Directory listing with compression
1474 deps [path] Show project dependencies
1475 discover Find uncompressed commands in shell history
1476 ghost [--json] Ghost Token report: find hidden token waste
1477 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1478 session Show adoption statistics
1479 session task <desc> Set current task
1480 session finding <summary> Record a finding
1481 session save Save current session
1482 session load [id] Load session (latest if no ID)
1483 knowledge remember <value> --category <c> --key <k> Store a fact
1484 knowledge recall [query] [--category <c>] Retrieve facts
1485 knowledge search <query> Cross-project knowledge search
1486 knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
1487 knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
1488 knowledge remove --category <c> --key <k> Remove a fact
1489 knowledge status Knowledge base summary
1490 overview [task] Project overview (task-contextualized if given)
1491 compress [--signatures] Context compression checkpoint
1492 config Show/edit configuration (~/.lean-ctx/config.toml)
1493 profile [list|show|diff|create|set] Manage context profiles
1494 theme [list|set|export|import] Customize terminal colors and themes
1495 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1496 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1497 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1498 update [--check] Self-update lean-ctx binary from GitHub Releases
1499 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1500 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1501 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1502 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1503 smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1504 Code smell detection (Property Graph, 8 rules)
1505 control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1506 plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
1507 compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1508 uninstall Remove shell hook, MCP configs, and data directory
1509
1510SHELL HOOK PATTERNS (95+):
1511 git status, log, diff, add, commit, push, pull, fetch, clone,
1512 branch, checkout, switch, merge, stash, tag, reset, remote
1513 docker build, ps, images, logs, compose, exec, network
1514 npm/pnpm install, test, run, list, outdated, audit
1515 cargo build, test, check, clippy
1516 gh pr list/view/create, issue list/view, run list/view
1517 kubectl get pods/services/deployments, logs, describe, apply
1518 python pip install/list/outdated, ruff check/format, poetry, uv
1519 linters eslint, biome, prettier, golangci-lint
1520 builds tsc, next build, vite build
1521 ruby rubocop, bundle install/update, rake test, rails test
1522 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1523 iac terraform, make, maven, gradle, dotnet, flutter, dart
1524 utils curl, grep/rg, find, ls, wget, env
1525 data JSON schema extraction, log deduplication
1526
1527READ MODES:
1528 auto Auto-select optimal mode (default)
1529 full Full content (cached re-reads = 13 tokens)
1530 map Dependency graph + API signatures
1531 signatures tree-sitter AST extraction (18 languages)
1532 task Task-relevant filtering (requires ctx_session task)
1533 reference One-line reference stub (cheap cache key)
1534 aggressive Syntax-stripped content
1535 entropy Shannon entropy filtered
1536 diff Changed lines only
1537 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1538
1539ENVIRONMENT:
1540 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1541 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1542 LEAN_CTX_RAW=1 Same as --raw for current command
1543 LEAN_CTX_AUTONOMY=false Disable autonomous features
1544 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1545
1546OPTIONS:
1547 --version, -V Show version
1548 --help, -h Show this help
1549
1550EXAMPLES:
1551 lean-ctx -c \"git status\" Compressed git output
1552 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1553 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1554 lean-ctx gain Visual terminal dashboard
1555 lean-ctx gain --live Live auto-updating terminal dashboard
1556 lean-ctx gain --graph 30-day savings chart
1557 lean-ctx gain --daily Day-by-day breakdown with USD
1558 lean-ctx token-report --json Machine-readable token + memory report
1559 lean-ctx dashboard Open web dashboard at localhost:3333
1560 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1561 lean-ctx gain --wrapped Wrapped report card (recommended)
1562 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1563 lean-ctx sessions list List all CCP sessions
1564 lean-ctx sessions show Show latest session state
1565 lean-ctx discover Find missed savings in shell history
1566 lean-ctx setup One-command setup (shell + editors + verify)
1567 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1568 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1569 lean-ctx bootstrap --json Machine-readable bootstrap report
1570 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1571 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1572 lean-ctx-off Disable all shell aliases
1573 lean-ctx-mode track Track mode: full output, stats recorded (default)
1574 lean-ctx-mode compress Compress mode: all output compressed (power users)
1575 lean-ctx-mode off Same as lean-ctx-off
1576 lean-ctx-status Show whether compression is active
1577 lean-ctx init --agent pi Install Pi Coding Agent extension
1578 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1579 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1580 lean-ctx doctor --fix --json Repair + machine-readable report
1581 lean-ctx status --json Machine-readable current status
1582 lean-ctx session task \"implement auth\"
1583 lean-ctx session finding \"auth.rs:42 — missing validation\"
1584 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1585 lean-ctx knowledge recall \"authentication\"
1586 lean-ctx knowledge search \"database migration\"
1587 lean-ctx overview \"refactor auth module\"
1588 lean-ctx compress --signatures
1589 lean-ctx read src/main.rs -m map
1590 lean-ctx grep \"pub fn\" src/
1591 lean-ctx deps .
1592
1593CLOUD:
1594 cloud status Show cloud connection status
1595 login <email> Log into existing LeanCTX Cloud account
1596 register <email> Create a new LeanCTX Cloud account
1597 forgot-password <email> Send password reset email
1598 sync Upload local stats to cloud dashboard
1599 contribute Share anonymized compression data
1600
1601TROUBLESHOOTING:
1602 Commands broken? lean-ctx-off (fixes current session)
1603 Permanent fix? lean-ctx uninstall (removes all hooks)
1604 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1605 Binary missing? Aliases auto-fallback to original commands (safe)
1606 Preview init? lean-ctx init --global --dry-run
1607
1608WEBSITE: https://leanctx.com
1609GITHUB: https://github.com/yvgude/lean-ctx
1610",
1611 version = env!("CARGO_PKG_VERSION"),
1612 );
1613}