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 if stop_mode {
685 if let Err(e) = crate::daemon::stop_daemon() {
686 eprintln!("Error: {e}");
687 std::process::exit(1);
688 }
689 return;
690 }
691
692 if status_mode {
693 println!("{}", crate::daemon::daemon_status());
694 return;
695 }
696
697 if daemon_mode {
698 if let Err(e) = crate::daemon::start_daemon(&rest) {
699 eprintln!("Error: {e}");
700 std::process::exit(1);
701 }
702 return;
703 }
704
705 if foreground_daemon {
706 if let Err(e) = crate::daemon::init_foreground_daemon() {
707 eprintln!("Error writing PID file: {e}");
708 std::process::exit(1);
709 }
710 let addr = crate::daemon::daemon_addr();
711 if let Err(e) = run_async(crate::http_server::serve_ipc(cfg.clone(), addr))
712 {
713 tracing::error!("Daemon server error: {e}");
714 crate::daemon::cleanup_daemon_files();
715 std::process::exit(1);
716 }
717 crate::daemon::cleanup_daemon_files();
718 return;
719 }
720
721 if cfg.auth_token.is_none() {
722 if let Ok(v) = std::env::var("LEAN_CTX_HTTP_TOKEN") {
723 if !v.trim().is_empty() {
724 cfg.auth_token = Some(v);
725 }
726 }
727 }
728
729 if let Err(e) = run_async(crate::http_server::serve(cfg)) {
730 tracing::error!("HTTP server error: {e}");
731 std::process::exit(1);
732 }
733 return;
734 }
735 #[cfg(not(feature = "http-server"))]
736 {
737 eprintln!("lean-ctx serve is not available in this build");
738 std::process::exit(1);
739 }
740 }
741 "watch" => {
742 if rest.iter().any(|a| a == "--help" || a == "-h") {
743 println!("Usage: lean-ctx watch");
744 println!(" Live TUI dashboard (real-time event stream).");
745 return;
746 }
747 if let Err(e) = tui::run() {
748 tracing::error!("TUI error: {e}");
749 std::process::exit(1);
750 }
751 return;
752 }
753 "proxy" => {
754 #[cfg(feature = "http-server")]
755 {
756 let sub = rest.first().map_or("help", std::string::String::as_str);
757 match sub {
758 "start" => {
759 let port: u16 = rest
760 .iter()
761 .find_map(|p| {
762 p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p="))
763 })
764 .and_then(|p| p.parse().ok())
765 .unwrap_or(4444);
766 let autostart = rest.iter().any(|a| a == "--autostart");
767 if autostart {
768 crate::proxy_autostart::install(port, false);
769 return;
770 }
771 if let Err(e) = run_async(crate::proxy::start_proxy(port)) {
772 tracing::error!("Proxy error: {e}");
773 std::process::exit(1);
774 }
775 }
776 "stop" => {
777 match ureq::get(&format!(
778 "http://127.0.0.1:{}/health",
779 rest.iter()
780 .find_map(|p| p.strip_prefix("--port="))
781 .and_then(|p| p.parse::<u16>().ok())
782 .unwrap_or(4444)
783 ))
784 .call()
785 {
786 Ok(_) => {
787 println!("Proxy is running. Use Ctrl+C or kill the process.");
788 }
789 Err(_) => {
790 println!("No proxy running on that port.");
791 }
792 }
793 }
794 "status" => {
795 let port: u16 = rest
796 .iter()
797 .find_map(|p| p.strip_prefix("--port="))
798 .and_then(|p| p.parse().ok())
799 .unwrap_or(4444);
800 if let Ok(resp) =
801 ureq::get(&format!("http://127.0.0.1:{port}/status")).call()
802 {
803 let body = resp.into_body().read_to_string().unwrap_or_default();
804 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
805 println!("lean-ctx proxy status:");
806 println!(" Requests: {}", v["requests_total"]);
807 println!(" Compressed: {}", v["requests_compressed"]);
808 println!(" Tokens saved: {}", v["tokens_saved"]);
809 println!(
810 " Compression: {}%",
811 v["compression_ratio_pct"].as_str().unwrap_or("0.0")
812 );
813 } else {
814 println!("{body}");
815 }
816 } else {
817 println!("No proxy running on port {port}.");
818 println!("Start with: lean-ctx proxy start");
819 }
820 }
821 _ => {
822 println!("Usage: lean-ctx proxy <start|stop|status> [--port=4444]");
823 }
824 }
825 return;
826 }
827 #[cfg(not(feature = "http-server"))]
828 {
829 eprintln!("lean-ctx proxy is not available in this build");
830 std::process::exit(1);
831 }
832 }
833 "init" => {
834 super::cmd_init(&rest);
835 return;
836 }
837 "setup" => {
838 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
839 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
840 let fix = rest.iter().any(|a| a == "--fix");
841 let json = rest.iter().any(|a| a == "--json");
842 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
843
844 if non_interactive || fix || json || yes {
845 let opts = setup::SetupOptions {
846 non_interactive,
847 yes,
848 fix,
849 json,
850 no_auto_approve,
851 };
852 match setup::run_setup_with_options(opts) {
853 Ok(report) => {
854 if json {
855 println!(
856 "{}",
857 serde_json::to_string_pretty(&report)
858 .unwrap_or_else(|_| "{}".to_string())
859 );
860 }
861 if !report.success {
862 std::process::exit(1);
863 }
864 }
865 Err(e) => {
866 eprintln!("{e}");
867 std::process::exit(1);
868 }
869 }
870 } else {
871 setup::run_setup();
872 }
873 return;
874 }
875 "install" => {
876 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
877 let json = rest.iter().any(|a| a == "--json");
878 if !repair {
879 eprintln!("Usage: lean-ctx install --repair [--json]");
880 std::process::exit(1);
881 }
882 let opts = setup::SetupOptions {
883 non_interactive: true,
884 yes: true,
885 fix: true,
886 json,
887 ..Default::default()
888 };
889 match setup::run_setup_with_options(opts) {
890 Ok(report) => {
891 if json {
892 println!(
893 "{}",
894 serde_json::to_string_pretty(&report)
895 .unwrap_or_else(|_| "{}".to_string())
896 );
897 }
898 if !report.success {
899 std::process::exit(1);
900 }
901 }
902 Err(e) => {
903 eprintln!("{e}");
904 std::process::exit(1);
905 }
906 }
907 return;
908 }
909 "bootstrap" => {
910 let json = rest.iter().any(|a| a == "--json");
911 let opts = setup::SetupOptions {
912 non_interactive: true,
913 yes: true,
914 fix: true,
915 json,
916 ..Default::default()
917 };
918 match setup::run_setup_with_options(opts) {
919 Ok(report) => {
920 if json {
921 println!(
922 "{}",
923 serde_json::to_string_pretty(&report)
924 .unwrap_or_else(|_| "{}".to_string())
925 );
926 }
927 if !report.success {
928 std::process::exit(1);
929 }
930 }
931 Err(e) => {
932 eprintln!("{e}");
933 std::process::exit(1);
934 }
935 }
936 return;
937 }
938 "status" => {
939 let code = status::run_cli(&rest);
940 if code != 0 {
941 std::process::exit(code);
942 }
943 return;
944 }
945 "read" => {
946 super::cmd_read(&rest);
947 core::stats::flush();
948 return;
949 }
950 "diff" => {
951 super::cmd_diff(&rest);
952 core::stats::flush();
953 return;
954 }
955 "grep" => {
956 super::cmd_grep(&rest);
957 core::stats::flush();
958 return;
959 }
960 "find" => {
961 super::cmd_find(&rest);
962 core::stats::flush();
963 return;
964 }
965 "ls" => {
966 super::cmd_ls(&rest);
967 core::stats::flush();
968 return;
969 }
970 "deps" => {
971 super::cmd_deps(&rest);
972 core::stats::flush();
973 return;
974 }
975 "discover" => {
976 super::cmd_discover(&rest);
977 return;
978 }
979 "ghost" => {
980 super::cmd_ghost(&rest);
981 return;
982 }
983 "filter" => {
984 super::cmd_filter(&rest);
985 return;
986 }
987 "heatmap" => {
988 heatmap::cmd_heatmap(&rest);
989 return;
990 }
991 "graph" => {
992 let sub = rest.first().map_or("build", std::string::String::as_str);
993 match sub {
994 "build" => {
995 let root = rest.get(1).cloned().or_else(|| {
996 std::env::current_dir()
997 .ok()
998 .map(|p| p.to_string_lossy().to_string())
999 });
1000 let root = root.unwrap_or_else(|| ".".to_string());
1001 let index = core::graph_index::load_or_build(&root);
1002 println!(
1003 "Graph built: {} files, {} edges",
1004 index.files.len(),
1005 index.edges.len()
1006 );
1007 }
1008 "export-html" => {
1009 let mut root: Option<String> = None;
1010 let mut out: Option<String> = None;
1011 let mut max_nodes: usize = 2500;
1012
1013 let args = &rest[1..];
1014 let mut i = 0usize;
1015 while i < args.len() {
1016 let a = args[i].as_str();
1017 if let Some(v) = a.strip_prefix("--root=") {
1018 root = Some(v.to_string());
1019 } else if a == "--root" {
1020 root = args.get(i + 1).cloned();
1021 i += 1;
1022 } else if let Some(v) = a.strip_prefix("--out=") {
1023 out = Some(v.to_string());
1024 } else if a == "--out" {
1025 out = args.get(i + 1).cloned();
1026 i += 1;
1027 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1028 max_nodes = v.parse::<usize>().unwrap_or(0);
1029 } else if a == "--max-nodes" {
1030 let v = args.get(i + 1).map_or("", String::as_str);
1031 max_nodes = v.parse::<usize>().unwrap_or(0);
1032 i += 1;
1033 }
1034 i += 1;
1035 }
1036
1037 let root = root
1038 .or_else(|| {
1039 std::env::current_dir()
1040 .ok()
1041 .map(|p| p.to_string_lossy().to_string())
1042 })
1043 .unwrap_or_else(|| ".".to_string());
1044 let Some(out) = out else {
1045 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1046 std::process::exit(1);
1047 };
1048 if max_nodes == 0 {
1049 eprintln!("--max-nodes must be >= 1");
1050 std::process::exit(1);
1051 }
1052
1053 core::graph_export::export_graph_html(
1054 &root,
1055 std::path::Path::new(&out),
1056 max_nodes,
1057 )
1058 .unwrap_or_else(|e| {
1059 eprintln!("graph export failed: {e}");
1060 std::process::exit(1);
1061 });
1062 println!("{out}");
1063 }
1064 _ => {
1065 eprintln!(
1066 "Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1067 );
1068 std::process::exit(1);
1069 }
1070 }
1071 return;
1072 }
1073 "smells" => {
1074 let action = rest.first().map_or("summary", String::as_str);
1075 let rule = rest.iter().enumerate().find_map(|(i, a)| {
1076 if let Some(v) = a.strip_prefix("--rule=") {
1077 return Some(v.to_string());
1078 }
1079 if a == "--rule" {
1080 return rest.get(i + 1).cloned();
1081 }
1082 None
1083 });
1084 let path = rest.iter().enumerate().find_map(|(i, a)| {
1085 if let Some(v) = a.strip_prefix("--path=") {
1086 return Some(v.to_string());
1087 }
1088 if a == "--path" {
1089 return rest.get(i + 1).cloned();
1090 }
1091 None
1092 });
1093 let root = rest
1094 .iter()
1095 .enumerate()
1096 .find_map(|(i, a)| {
1097 if let Some(v) = a.strip_prefix("--root=") {
1098 return Some(v.to_string());
1099 }
1100 if a == "--root" {
1101 return rest.get(i + 1).cloned();
1102 }
1103 None
1104 })
1105 .or_else(|| {
1106 std::env::current_dir()
1107 .ok()
1108 .map(|p| p.to_string_lossy().to_string())
1109 })
1110 .unwrap_or_else(|| ".".to_string());
1111 let fmt = if rest.iter().any(|a| a == "--json") {
1112 Some("json")
1113 } else {
1114 None
1115 };
1116 println!(
1117 "{}",
1118 tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1119 );
1120 return;
1121 }
1122 "session" => {
1123 super::cmd_session_action(&rest);
1124 return;
1125 }
1126 "control" | "context-control" => {
1127 super::cmd_control(&rest);
1128 return;
1129 }
1130 "plan" | "context-plan" => {
1131 super::cmd_plan(&rest);
1132 return;
1133 }
1134 "compile" | "context-compile" => {
1135 super::cmd_compile(&rest);
1136 return;
1137 }
1138 "knowledge" => {
1139 super::cmd_knowledge(&rest);
1140 return;
1141 }
1142 "overview" => {
1143 super::cmd_overview(&rest);
1144 return;
1145 }
1146 "compress" => {
1147 super::cmd_compress(&rest);
1148 return;
1149 }
1150 "wrapped" => {
1151 super::cmd_wrapped(&rest);
1152 return;
1153 }
1154 "sessions" => {
1155 super::cmd_sessions(&rest);
1156 return;
1157 }
1158 "benchmark" => {
1159 super::cmd_benchmark(&rest);
1160 return;
1161 }
1162 "profile" => {
1163 super::cmd_profile(&rest);
1164 return;
1165 }
1166 "config" => {
1167 super::cmd_config(&rest);
1168 return;
1169 }
1170 "stats" => {
1171 super::cmd_stats(&rest);
1172 return;
1173 }
1174 "cache" => {
1175 super::cmd_cache(&rest);
1176 return;
1177 }
1178 "theme" => {
1179 super::cmd_theme(&rest);
1180 return;
1181 }
1182 "tee" => {
1183 super::cmd_tee(&rest);
1184 return;
1185 }
1186 "terse" | "compression" => {
1187 super::cmd_compression(&rest);
1188 return;
1189 }
1190 "slow-log" => {
1191 super::cmd_slow_log(&rest);
1192 return;
1193 }
1194 "update" | "--self-update" => {
1195 core::updater::run(&rest);
1196 return;
1197 }
1198 "doctor" => {
1199 let code = doctor::run_cli(&rest);
1200 if code != 0 {
1201 std::process::exit(code);
1202 }
1203 return;
1204 }
1205 "gotchas" | "bugs" => {
1206 super::cloud::cmd_gotchas(&rest);
1207 return;
1208 }
1209 "learn" => {
1210 super::cmd_learn(&rest);
1211 return;
1212 }
1213 "buddy" | "pet" => {
1214 super::cloud::cmd_buddy(&rest);
1215 return;
1216 }
1217 "hook" => {
1218 hook_handlers::mark_hook_environment();
1219 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1220 let action = rest.first().map_or("help", std::string::String::as_str);
1221 match action {
1222 "rewrite" => hook_handlers::handle_rewrite(),
1223 "redirect" => hook_handlers::handle_redirect(),
1224 "copilot" => hook_handlers::handle_copilot(),
1225 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1226 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1227 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1228 _ => {
1229 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1230 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1231 std::process::exit(1);
1232 }
1233 }
1234 return;
1235 }
1236 "report-issue" | "report" => {
1237 report::run(&rest);
1238 return;
1239 }
1240 "uninstall" => {
1241 let dry_run = rest.iter().any(|a| a == "--dry-run");
1242 uninstall::run(dry_run);
1243 return;
1244 }
1245 "bypass" => {
1246 if rest.is_empty() {
1247 eprintln!("Usage: lean-ctx bypass \"command\"");
1248 eprintln!("Runs the command with zero compression (raw passthrough).");
1249 std::process::exit(1);
1250 }
1251 let command = if rest.len() == 1 {
1252 rest[0].clone()
1253 } else {
1254 shell::join_command(&args[2..])
1255 };
1256 std::env::set_var("LEAN_CTX_RAW", "1");
1257 let code = shell::exec(&command);
1258 std::process::exit(code);
1259 }
1260 "safety-levels" | "safety" => {
1261 println!("{}", core::compression_safety::format_safety_table());
1262 return;
1263 }
1264 "cheat" | "cheatsheet" | "cheat-sheet" => {
1265 super::cmd_cheatsheet();
1266 return;
1267 }
1268 "login" => {
1269 super::cloud::cmd_login(&rest);
1270 return;
1271 }
1272 "register" => {
1273 super::cloud::cmd_register(&rest);
1274 return;
1275 }
1276 "forgot-password" => {
1277 super::cloud::cmd_forgot_password(&rest);
1278 return;
1279 }
1280 "sync" => {
1281 super::cloud::cmd_sync();
1282 return;
1283 }
1284 "contribute" => {
1285 super::cloud::cmd_contribute();
1286 return;
1287 }
1288 "cloud" => {
1289 super::cloud::cmd_cloud(&rest);
1290 return;
1291 }
1292 "upgrade" => {
1293 super::cloud::cmd_upgrade();
1294 return;
1295 }
1296 "--version" | "-V" => {
1297 println!("{}", core::integrity::origin_line());
1298 return;
1299 }
1300 "--help" | "-h" => {
1301 print_help();
1302 return;
1303 }
1304 "mcp" => {}
1305 _ => {
1306 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1307 print_help();
1308 std::process::exit(1);
1309 }
1310 }
1311 }
1312
1313 if let Err(e) = run_mcp_server() {
1314 tracing::error!("lean-ctx: {e}");
1315 std::process::exit(1);
1316 }
1317}
1318
1319fn passthrough(command: &str) -> ! {
1320 let (shell, flag) = shell::shell_and_flag();
1321 let status = std::process::Command::new(&shell)
1322 .arg(&flag)
1323 .arg(command)
1324 .env("LEAN_CTX_ACTIVE", "1")
1325 .status()
1326 .map_or(127, |s| s.code().unwrap_or(1));
1327 std::process::exit(status);
1328}
1329
1330fn run_async<F: std::future::Future>(future: F) -> F::Output {
1331 tokio::runtime::Runtime::new()
1332 .expect("failed to create async runtime")
1333 .block_on(future)
1334}
1335
1336fn run_mcp_server() -> Result<()> {
1337 use rmcp::ServiceExt;
1338
1339 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1340
1341 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1342
1343 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1347 "mcp-startup",
1348 std::time::Duration::from_secs(3),
1349 std::time::Duration::from_secs(30),
1350 );
1351
1352 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1353 let worker_threads = parallelism.clamp(1, 4);
1354 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1355
1356 let rt = tokio::runtime::Builder::new_multi_thread()
1357 .worker_threads(worker_threads)
1358 .max_blocking_threads(max_blocking_threads)
1359 .enable_all()
1360 .build()?;
1361
1362 let server = tools::create_server();
1363 drop(startup_lock);
1364
1365 rt.block_on(async {
1366 core::logging::init_mcp_logging();
1367 core::protocol::set_mcp_context(true);
1368
1369 tracing::info!(
1370 "lean-ctx v{} MCP server starting",
1371 env!("CARGO_PKG_VERSION")
1372 );
1373
1374 let transport =
1375 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1376 let server_handle = server.clone();
1377 let service = match server.serve(transport).await {
1378 Ok(s) => s,
1379 Err(e) => {
1380 let msg = e.to_string();
1381 if msg.contains("expect initialized")
1382 || msg.contains("context canceled")
1383 || msg.contains("broken pipe")
1384 {
1385 tracing::debug!("Client disconnected before init: {msg}");
1386 return Ok(());
1387 }
1388 return Err(e.into());
1389 }
1390 };
1391 match service.waiting().await {
1392 Ok(reason) => {
1393 tracing::info!("MCP server stopped: {reason:?}");
1394 }
1395 Err(e) => {
1396 let msg = e.to_string();
1397 if msg.contains("broken pipe")
1398 || msg.contains("connection reset")
1399 || msg.contains("context canceled")
1400 {
1401 tracing::info!("MCP server: transport closed ({msg})");
1402 } else {
1403 tracing::error!("MCP server error: {msg}");
1404 }
1405 }
1406 }
1407
1408 server_handle.shutdown().await;
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 | 59 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}