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