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
855 if non_interactive || fix || json || yes {
856 let opts = setup::SetupOptions {
857 non_interactive,
858 yes,
859 fix,
860 json,
861 };
862 match setup::run_setup_with_options(opts) {
863 Ok(report) => {
864 if json {
865 println!(
866 "{}",
867 serde_json::to_string_pretty(&report)
868 .unwrap_or_else(|_| "{}".to_string())
869 );
870 }
871 if !report.success {
872 std::process::exit(1);
873 }
874 }
875 Err(e) => {
876 eprintln!("{e}");
877 std::process::exit(1);
878 }
879 }
880 } else {
881 setup::run_setup();
882 }
883 return;
884 }
885 "install" => {
886 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
887 let json = rest.iter().any(|a| a == "--json");
888 if !repair {
889 eprintln!("Usage: lean-ctx install --repair [--json]");
890 std::process::exit(1);
891 }
892 let opts = setup::SetupOptions {
893 non_interactive: true,
894 yes: true,
895 fix: true,
896 json,
897 };
898 match setup::run_setup_with_options(opts) {
899 Ok(report) => {
900 if json {
901 println!(
902 "{}",
903 serde_json::to_string_pretty(&report)
904 .unwrap_or_else(|_| "{}".to_string())
905 );
906 }
907 if !report.success {
908 std::process::exit(1);
909 }
910 }
911 Err(e) => {
912 eprintln!("{e}");
913 std::process::exit(1);
914 }
915 }
916 return;
917 }
918 "bootstrap" => {
919 let json = rest.iter().any(|a| a == "--json");
920 let opts = setup::SetupOptions {
921 non_interactive: true,
922 yes: true,
923 fix: true,
924 json,
925 };
926 match setup::run_setup_with_options(opts) {
927 Ok(report) => {
928 if json {
929 println!(
930 "{}",
931 serde_json::to_string_pretty(&report)
932 .unwrap_or_else(|_| "{}".to_string())
933 );
934 }
935 if !report.success {
936 std::process::exit(1);
937 }
938 }
939 Err(e) => {
940 eprintln!("{e}");
941 std::process::exit(1);
942 }
943 }
944 return;
945 }
946 "status" => {
947 let code = status::run_cli(&rest);
948 if code != 0 {
949 std::process::exit(code);
950 }
951 return;
952 }
953 "read" => {
954 super::cmd_read(&rest);
955 core::stats::flush();
956 return;
957 }
958 "diff" => {
959 super::cmd_diff(&rest);
960 core::stats::flush();
961 return;
962 }
963 "grep" => {
964 super::cmd_grep(&rest);
965 core::stats::flush();
966 return;
967 }
968 "find" => {
969 super::cmd_find(&rest);
970 core::stats::flush();
971 return;
972 }
973 "ls" => {
974 super::cmd_ls(&rest);
975 core::stats::flush();
976 return;
977 }
978 "deps" => {
979 super::cmd_deps(&rest);
980 core::stats::flush();
981 return;
982 }
983 "discover" => {
984 super::cmd_discover(&rest);
985 return;
986 }
987 "ghost" => {
988 super::cmd_ghost(&rest);
989 return;
990 }
991 "filter" => {
992 super::cmd_filter(&rest);
993 return;
994 }
995 "heatmap" => {
996 heatmap::cmd_heatmap(&rest);
997 return;
998 }
999 "graph" => {
1000 let sub = rest.first().map_or("build", std::string::String::as_str);
1001 match sub {
1002 "build" => {
1003 let root = rest.get(1).cloned().or_else(|| {
1004 std::env::current_dir()
1005 .ok()
1006 .map(|p| p.to_string_lossy().to_string())
1007 });
1008 let root = root.unwrap_or_else(|| ".".to_string());
1009 let index = core::graph_index::load_or_build(&root);
1010 println!(
1011 "Graph built: {} files, {} edges",
1012 index.files.len(),
1013 index.edges.len()
1014 );
1015 }
1016 "export-html" => {
1017 let mut root: Option<String> = None;
1018 let mut out: Option<String> = None;
1019 let mut max_nodes: usize = 2500;
1020
1021 let args = &rest[1..];
1022 let mut i = 0usize;
1023 while i < args.len() {
1024 let a = args[i].as_str();
1025 if let Some(v) = a.strip_prefix("--root=") {
1026 root = Some(v.to_string());
1027 } else if a == "--root" {
1028 root = args.get(i + 1).cloned();
1029 i += 1;
1030 } else if let Some(v) = a.strip_prefix("--out=") {
1031 out = Some(v.to_string());
1032 } else if a == "--out" {
1033 out = args.get(i + 1).cloned();
1034 i += 1;
1035 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1036 max_nodes = v.parse::<usize>().unwrap_or(0);
1037 } else if a == "--max-nodes" {
1038 let v = args.get(i + 1).map_or("", String::as_str);
1039 max_nodes = v.parse::<usize>().unwrap_or(0);
1040 i += 1;
1041 }
1042 i += 1;
1043 }
1044
1045 let root = root
1046 .or_else(|| {
1047 std::env::current_dir()
1048 .ok()
1049 .map(|p| p.to_string_lossy().to_string())
1050 })
1051 .unwrap_or_else(|| ".".to_string());
1052 let Some(out) = out else {
1053 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1054 std::process::exit(1);
1055 };
1056 if max_nodes == 0 {
1057 eprintln!("--max-nodes must be >= 1");
1058 std::process::exit(1);
1059 }
1060
1061 core::graph_export::export_graph_html(
1062 &root,
1063 std::path::Path::new(&out),
1064 max_nodes,
1065 )
1066 .unwrap_or_else(|e| {
1067 eprintln!("graph export failed: {e}");
1068 std::process::exit(1);
1069 });
1070 println!("{out}");
1071 }
1072 _ => {
1073 eprintln!(
1074 "Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1075 );
1076 std::process::exit(1);
1077 }
1078 }
1079 return;
1080 }
1081 "smells" => {
1082 let action = rest.first().map_or("summary", String::as_str);
1083 let rule = rest.iter().enumerate().find_map(|(i, a)| {
1084 if let Some(v) = a.strip_prefix("--rule=") {
1085 return Some(v.to_string());
1086 }
1087 if a == "--rule" {
1088 return rest.get(i + 1).cloned();
1089 }
1090 None
1091 });
1092 let path = rest.iter().enumerate().find_map(|(i, a)| {
1093 if let Some(v) = a.strip_prefix("--path=") {
1094 return Some(v.to_string());
1095 }
1096 if a == "--path" {
1097 return rest.get(i + 1).cloned();
1098 }
1099 None
1100 });
1101 let root = rest
1102 .iter()
1103 .enumerate()
1104 .find_map(|(i, a)| {
1105 if let Some(v) = a.strip_prefix("--root=") {
1106 return Some(v.to_string());
1107 }
1108 if a == "--root" {
1109 return rest.get(i + 1).cloned();
1110 }
1111 None
1112 })
1113 .or_else(|| {
1114 std::env::current_dir()
1115 .ok()
1116 .map(|p| p.to_string_lossy().to_string())
1117 })
1118 .unwrap_or_else(|| ".".to_string());
1119 let fmt = if rest.iter().any(|a| a == "--json") {
1120 Some("json")
1121 } else {
1122 None
1123 };
1124 println!(
1125 "{}",
1126 tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1127 );
1128 return;
1129 }
1130 "session" => {
1131 super::cmd_session_action(&rest);
1132 return;
1133 }
1134 "control" | "context-control" => {
1135 super::cmd_control(&rest);
1136 return;
1137 }
1138 "plan" | "context-plan" => {
1139 super::cmd_plan(&rest);
1140 return;
1141 }
1142 "compile" | "context-compile" => {
1143 super::cmd_compile(&rest);
1144 return;
1145 }
1146 "knowledge" => {
1147 super::cmd_knowledge(&rest);
1148 return;
1149 }
1150 "overview" => {
1151 super::cmd_overview(&rest);
1152 return;
1153 }
1154 "compress" => {
1155 super::cmd_compress(&rest);
1156 return;
1157 }
1158 "wrapped" => {
1159 super::cmd_wrapped(&rest);
1160 return;
1161 }
1162 "sessions" => {
1163 super::cmd_sessions(&rest);
1164 return;
1165 }
1166 "benchmark" => {
1167 super::cmd_benchmark(&rest);
1168 return;
1169 }
1170 "profile" => {
1171 super::cmd_profile(&rest);
1172 return;
1173 }
1174 "config" => {
1175 super::cmd_config(&rest);
1176 return;
1177 }
1178 "stats" => {
1179 super::cmd_stats(&rest);
1180 return;
1181 }
1182 "cache" => {
1183 super::cmd_cache(&rest);
1184 return;
1185 }
1186 "theme" => {
1187 super::cmd_theme(&rest);
1188 return;
1189 }
1190 "tee" => {
1191 super::cmd_tee(&rest);
1192 return;
1193 }
1194 "terse" | "compression" => {
1195 super::cmd_compression(&rest);
1196 return;
1197 }
1198 "slow-log" => {
1199 super::cmd_slow_log(&rest);
1200 return;
1201 }
1202 "update" | "--self-update" => {
1203 core::updater::run(&rest);
1204 return;
1205 }
1206 "doctor" => {
1207 let code = doctor::run_cli(&rest);
1208 if code != 0 {
1209 std::process::exit(code);
1210 }
1211 return;
1212 }
1213 "gotchas" | "bugs" => {
1214 super::cloud::cmd_gotchas(&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 "copilot" => hook_handlers::handle_copilot(),
1229 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1230 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1231 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1232 _ => {
1233 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1234 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1235 std::process::exit(1);
1236 }
1237 }
1238 return;
1239 }
1240 "report-issue" | "report" => {
1241 report::run(&rest);
1242 return;
1243 }
1244 "uninstall" => {
1245 let dry_run = rest.iter().any(|a| a == "--dry-run");
1246 uninstall::run(dry_run);
1247 return;
1248 }
1249 "bypass" => {
1250 if rest.is_empty() {
1251 eprintln!("Usage: lean-ctx bypass \"command\"");
1252 eprintln!("Runs the command with zero compression (raw passthrough).");
1253 std::process::exit(1);
1254 }
1255 let command = if rest.len() == 1 {
1256 rest[0].clone()
1257 } else {
1258 shell::join_command(&args[2..])
1259 };
1260 std::env::set_var("LEAN_CTX_RAW", "1");
1261 let code = shell::exec(&command);
1262 std::process::exit(code);
1263 }
1264 "safety-levels" | "safety" => {
1265 println!("{}", core::compression_safety::format_safety_table());
1266 return;
1267 }
1268 "cheat" | "cheatsheet" | "cheat-sheet" => {
1269 super::cmd_cheatsheet();
1270 return;
1271 }
1272 "login" => {
1273 super::cloud::cmd_login(&rest);
1274 return;
1275 }
1276 "register" => {
1277 super::cloud::cmd_register(&rest);
1278 return;
1279 }
1280 "forgot-password" => {
1281 super::cloud::cmd_forgot_password(&rest);
1282 return;
1283 }
1284 "sync" => {
1285 super::cloud::cmd_sync();
1286 return;
1287 }
1288 "contribute" => {
1289 super::cloud::cmd_contribute();
1290 return;
1291 }
1292 "cloud" => {
1293 super::cloud::cmd_cloud(&rest);
1294 return;
1295 }
1296 "upgrade" => {
1297 super::cloud::cmd_upgrade();
1298 return;
1299 }
1300 "--version" | "-V" => {
1301 println!("{}", core::integrity::origin_line());
1302 return;
1303 }
1304 "--help" | "-h" => {
1305 print_help();
1306 return;
1307 }
1308 "mcp" => {}
1309 _ => {
1310 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1311 print_help();
1312 std::process::exit(1);
1313 }
1314 }
1315 }
1316
1317 if let Err(e) = run_mcp_server() {
1318 tracing::error!("lean-ctx: {e}");
1319 std::process::exit(1);
1320 }
1321}
1322
1323fn passthrough(command: &str) -> ! {
1324 let (shell, flag) = shell::shell_and_flag();
1325 let status = std::process::Command::new(&shell)
1326 .arg(&flag)
1327 .arg(command)
1328 .env("LEAN_CTX_ACTIVE", "1")
1329 .status()
1330 .map_or(127, |s| s.code().unwrap_or(1));
1331 std::process::exit(status);
1332}
1333
1334fn run_async<F: std::future::Future>(future: F) -> F::Output {
1335 tokio::runtime::Runtime::new()
1336 .expect("failed to create async runtime")
1337 .block_on(future)
1338}
1339
1340fn run_mcp_server() -> Result<()> {
1341 use rmcp::ServiceExt;
1342
1343 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1344
1345 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1346
1347 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1351 "mcp-startup",
1352 std::time::Duration::from_secs(3),
1353 std::time::Duration::from_secs(30),
1354 );
1355
1356 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1357 let worker_threads = parallelism.clamp(1, 4);
1358 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1359
1360 let rt = tokio::runtime::Builder::new_multi_thread()
1361 .worker_threads(worker_threads)
1362 .max_blocking_threads(max_blocking_threads)
1363 .enable_all()
1364 .build()?;
1365
1366 let server = tools::create_server();
1367 drop(startup_lock);
1368
1369 rt.block_on(async {
1370 core::logging::init_mcp_logging();
1371
1372 tracing::info!(
1373 "lean-ctx v{} MCP server starting",
1374 env!("CARGO_PKG_VERSION")
1375 );
1376
1377 let transport =
1378 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1379 let service = match server.serve(transport).await {
1380 Ok(s) => s,
1381 Err(e) => {
1382 let msg = e.to_string();
1383 if msg.contains("expect initialized")
1384 || msg.contains("context canceled")
1385 || msg.contains("broken pipe")
1386 {
1387 tracing::debug!("Client disconnected before init: {msg}");
1388 return Ok(());
1389 }
1390 return Err(e.into());
1391 }
1392 };
1393 match service.waiting().await {
1394 Ok(reason) => {
1395 tracing::info!("MCP server stopped: {reason:?}");
1396 }
1397 Err(e) => {
1398 let msg = e.to_string();
1399 if msg.contains("broken pipe")
1400 || msg.contains("connection reset")
1401 || msg.contains("context canceled")
1402 {
1403 tracing::info!("MCP server: transport closed ({msg})");
1404 } else {
1405 tracing::error!("MCP server error: {msg}");
1406 }
1407 }
1408 }
1409
1410 core::stats::flush();
1411 core::heatmap::flush();
1412 core::mode_predictor::ModePredictor::flush();
1413 core::feedback::FeedbackStore::flush();
1414
1415 Ok(())
1416 })
1417}
1418
1419fn print_help() {
1420 println!(
1421 "lean-ctx {version} — Context Runtime for AI Agents
1422
142395+ compression patterns | 58 MCP tools | Context Continuity Protocol
1424
1425USAGE:
1426 lean-ctx Start MCP server (stdio)
1427 lean-ctx serve Start MCP server (Streamable HTTP)
1428 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1429 lean-ctx serve --stop Stop running daemon
1430 lean-ctx serve --status Show daemon status
1431 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1432 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1433 lean-ctx -c --raw \"command\" Execute without compression (full output)
1434 lean-ctx exec \"command\" Same as -c
1435 lean-ctx shell Interactive shell with compression
1436
1437COMMANDS:
1438 gain Visual dashboard (colors, bars, sparklines, USD)
1439 gain --live Live mode: auto-refreshes every 1s in-place
1440 gain --graph 30-day savings chart
1441 gain --daily Bordered day-by-day table with USD
1442 gain --json Raw JSON export of all stats
1443 token-report [--json] Token + memory report (project + session + CEP)
1444 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1445 index <status|build|build-full|watch> Codebase index utilities
1446 cep CEP impact report (score trends, cache, modes)
1447 watch Live TUI dashboard (real-time event stream)
1448 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1449 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1450 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1451 proxy status Show proxy statistics
1452 cache [list|clear|stats] Show/manage file read cache
1453 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1454 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1455 benchmark run [path] [--json] Run real benchmark on project files
1456 benchmark report [path] Generate shareable Markdown report
1457 cheatsheet Command cheat sheet & workflow quick reference
1458 setup One-command setup: shell + editor + verify
1459 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1460 bootstrap Non-interactive setup + fix (zero-config)
1461 status [--json] Show setup + MCP + rules status
1462 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1463 init --agent <name> Configure MCP for specific editor/agent
1464 read <file> [-m mode] Read file with compression
1465 diff <file1> <file2> Compressed file diff
1466 grep <pattern> [path] Search with compressed output
1467 find <pattern> [path] Find files with compressed output
1468 ls [path] Directory listing with compression
1469 deps [path] Show project dependencies
1470 discover Find uncompressed commands in shell history
1471 ghost [--json] Ghost Token report: find hidden token waste
1472 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1473 session Show adoption statistics
1474 session task <desc> Set current task
1475 session finding <summary> Record a finding
1476 session save Save current session
1477 session load [id] Load session (latest if no ID)
1478 knowledge remember <value> --category <c> --key <k> Store a fact
1479 knowledge recall [query] [--category <c>] Retrieve facts
1480 knowledge search <query> Cross-project knowledge search
1481 knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
1482 knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
1483 knowledge remove --category <c> --key <k> Remove a fact
1484 knowledge status Knowledge base summary
1485 overview [task] Project overview (task-contextualized if given)
1486 compress [--signatures] Context compression checkpoint
1487 config Show/edit configuration (~/.lean-ctx/config.toml)
1488 profile [list|show|diff|create|set] Manage context profiles
1489 theme [list|set|export|import] Customize terminal colors and themes
1490 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1491 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1492 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1493 update [--check] Self-update lean-ctx binary from GitHub Releases
1494 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1495 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1496 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1497 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1498 smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1499 Code smell detection (Property Graph, 8 rules)
1500 control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1501 plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
1502 compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1503 uninstall Remove shell hook, MCP configs, and data directory
1504
1505SHELL HOOK PATTERNS (95+):
1506 git status, log, diff, add, commit, push, pull, fetch, clone,
1507 branch, checkout, switch, merge, stash, tag, reset, remote
1508 docker build, ps, images, logs, compose, exec, network
1509 npm/pnpm install, test, run, list, outdated, audit
1510 cargo build, test, check, clippy
1511 gh pr list/view/create, issue list/view, run list/view
1512 kubectl get pods/services/deployments, logs, describe, apply
1513 python pip install/list/outdated, ruff check/format, poetry, uv
1514 linters eslint, biome, prettier, golangci-lint
1515 builds tsc, next build, vite build
1516 ruby rubocop, bundle install/update, rake test, rails test
1517 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1518 iac terraform, make, maven, gradle, dotnet, flutter, dart
1519 utils curl, grep/rg, find, ls, wget, env
1520 data JSON schema extraction, log deduplication
1521
1522READ MODES:
1523 auto Auto-select optimal mode (default)
1524 full Full content (cached re-reads = 13 tokens)
1525 map Dependency graph + API signatures
1526 signatures tree-sitter AST extraction (18 languages)
1527 task Task-relevant filtering (requires ctx_session task)
1528 reference One-line reference stub (cheap cache key)
1529 aggressive Syntax-stripped content
1530 entropy Shannon entropy filtered
1531 diff Changed lines only
1532 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1533
1534ENVIRONMENT:
1535 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1536 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1537 LEAN_CTX_RAW=1 Same as --raw for current command
1538 LEAN_CTX_AUTONOMY=false Disable autonomous features
1539 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1540
1541OPTIONS:
1542 --version, -V Show version
1543 --help, -h Show this help
1544
1545EXAMPLES:
1546 lean-ctx -c \"git status\" Compressed git output
1547 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1548 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1549 lean-ctx gain Visual terminal dashboard
1550 lean-ctx gain --live Live auto-updating terminal dashboard
1551 lean-ctx gain --graph 30-day savings chart
1552 lean-ctx gain --daily Day-by-day breakdown with USD
1553 lean-ctx token-report --json Machine-readable token + memory report
1554 lean-ctx dashboard Open web dashboard at localhost:3333
1555 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1556 lean-ctx gain --wrapped Wrapped report card (recommended)
1557 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1558 lean-ctx sessions list List all CCP sessions
1559 lean-ctx sessions show Show latest session state
1560 lean-ctx discover Find missed savings in shell history
1561 lean-ctx setup One-command setup (shell + editors + verify)
1562 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1563 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1564 lean-ctx bootstrap --json Machine-readable bootstrap report
1565 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1566 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1567 lean-ctx-off Disable all shell aliases
1568 lean-ctx-mode track Track mode: full output, stats recorded (default)
1569 lean-ctx-mode compress Compress mode: all output compressed (power users)
1570 lean-ctx-mode off Same as lean-ctx-off
1571 lean-ctx-status Show whether compression is active
1572 lean-ctx init --agent pi Install Pi Coding Agent extension
1573 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1574 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1575 lean-ctx doctor --fix --json Repair + machine-readable report
1576 lean-ctx status --json Machine-readable current status
1577 lean-ctx session task \"implement auth\"
1578 lean-ctx session finding \"auth.rs:42 — missing validation\"
1579 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1580 lean-ctx knowledge recall \"authentication\"
1581 lean-ctx knowledge search \"database migration\"
1582 lean-ctx overview \"refactor auth module\"
1583 lean-ctx compress --signatures
1584 lean-ctx read src/main.rs -m map
1585 lean-ctx grep \"pub fn\" src/
1586 lean-ctx deps .
1587
1588CLOUD:
1589 cloud status Show cloud connection status
1590 login <email> Log into existing LeanCTX Cloud account
1591 register <email> Create a new LeanCTX Cloud account
1592 forgot-password <email> Send password reset email
1593 sync Upload local stats to cloud dashboard
1594 contribute Share anonymized compression data
1595
1596TROUBLESHOOTING:
1597 Commands broken? lean-ctx-off (fixes current session)
1598 Permanent fix? lean-ctx uninstall (removes all hooks)
1599 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1600 Binary missing? Aliases auto-fallback to original commands (safe)
1601 Preview init? lean-ctx init --global --dry-run
1602
1603WEBSITE: https://leanctx.com
1604GITHUB: https://github.com/yvgude/lean-ctx
1605",
1606 version = env!("CARGO_PKG_VERSION"),
1607 );
1608}