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 "session" => {
1082 super::cmd_session_action(&rest);
1083 return;
1084 }
1085 "control" | "context-control" => {
1086 super::cmd_control(&rest);
1087 return;
1088 }
1089 "plan" | "context-plan" => {
1090 super::cmd_plan(&rest);
1091 return;
1092 }
1093 "compile" | "context-compile" => {
1094 super::cmd_compile(&rest);
1095 return;
1096 }
1097 "knowledge" => {
1098 super::cmd_knowledge(&rest);
1099 return;
1100 }
1101 "overview" => {
1102 super::cmd_overview(&rest);
1103 return;
1104 }
1105 "compress" => {
1106 super::cmd_compress(&rest);
1107 return;
1108 }
1109 "wrapped" => {
1110 super::cmd_wrapped(&rest);
1111 return;
1112 }
1113 "sessions" => {
1114 super::cmd_sessions(&rest);
1115 return;
1116 }
1117 "benchmark" => {
1118 super::cmd_benchmark(&rest);
1119 return;
1120 }
1121 "profile" => {
1122 super::cmd_profile(&rest);
1123 return;
1124 }
1125 "config" => {
1126 super::cmd_config(&rest);
1127 return;
1128 }
1129 "stats" => {
1130 super::cmd_stats(&rest);
1131 return;
1132 }
1133 "cache" => {
1134 super::cmd_cache(&rest);
1135 return;
1136 }
1137 "theme" => {
1138 super::cmd_theme(&rest);
1139 return;
1140 }
1141 "tee" => {
1142 super::cmd_tee(&rest);
1143 return;
1144 }
1145 "terse" => {
1146 super::cmd_terse(&rest);
1147 return;
1148 }
1149 "slow-log" => {
1150 super::cmd_slow_log(&rest);
1151 return;
1152 }
1153 "update" | "--self-update" => {
1154 core::updater::run(&rest);
1155 return;
1156 }
1157 "doctor" => {
1158 let code = doctor::run_cli(&rest);
1159 if code != 0 {
1160 std::process::exit(code);
1161 }
1162 return;
1163 }
1164 "gotchas" | "bugs" => {
1165 super::cloud::cmd_gotchas(&rest);
1166 return;
1167 }
1168 "buddy" | "pet" => {
1169 super::cloud::cmd_buddy(&rest);
1170 return;
1171 }
1172 "hook" => {
1173 hook_handlers::mark_hook_environment();
1174 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1175 let action = rest.first().map_or("help", std::string::String::as_str);
1176 match action {
1177 "rewrite" => hook_handlers::handle_rewrite(),
1178 "redirect" => hook_handlers::handle_redirect(),
1179 "copilot" => hook_handlers::handle_copilot(),
1180 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1181 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1182 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1183 _ => {
1184 eprintln!("Usage: lean-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1185 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1186 std::process::exit(1);
1187 }
1188 }
1189 return;
1190 }
1191 "report-issue" | "report" => {
1192 report::run(&rest);
1193 return;
1194 }
1195 "uninstall" => {
1196 let dry_run = rest.iter().any(|a| a == "--dry-run");
1197 uninstall::run(dry_run);
1198 return;
1199 }
1200 "bypass" => {
1201 if rest.is_empty() {
1202 eprintln!("Usage: lean-ctx bypass \"command\"");
1203 eprintln!("Runs the command with zero compression (raw passthrough).");
1204 std::process::exit(1);
1205 }
1206 let command = if rest.len() == 1 {
1207 rest[0].clone()
1208 } else {
1209 shell::join_command(&args[2..])
1210 };
1211 std::env::set_var("LEAN_CTX_RAW", "1");
1212 let code = shell::exec(&command);
1213 std::process::exit(code);
1214 }
1215 "safety-levels" | "safety" => {
1216 println!("{}", core::compression_safety::format_safety_table());
1217 return;
1218 }
1219 "cheat" | "cheatsheet" | "cheat-sheet" => {
1220 super::cmd_cheatsheet();
1221 return;
1222 }
1223 "login" => {
1224 super::cloud::cmd_login(&rest);
1225 return;
1226 }
1227 "register" => {
1228 super::cloud::cmd_register(&rest);
1229 return;
1230 }
1231 "forgot-password" => {
1232 super::cloud::cmd_forgot_password(&rest);
1233 return;
1234 }
1235 "sync" => {
1236 super::cloud::cmd_sync();
1237 return;
1238 }
1239 "contribute" => {
1240 super::cloud::cmd_contribute();
1241 return;
1242 }
1243 "cloud" => {
1244 super::cloud::cmd_cloud(&rest);
1245 return;
1246 }
1247 "upgrade" => {
1248 super::cloud::cmd_upgrade();
1249 return;
1250 }
1251 "--version" | "-V" => {
1252 println!("{}", core::integrity::origin_line());
1253 return;
1254 }
1255 "--help" | "-h" => {
1256 print_help();
1257 return;
1258 }
1259 "mcp" => {}
1260 _ => {
1261 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1262 print_help();
1263 std::process::exit(1);
1264 }
1265 }
1266 }
1267
1268 if let Err(e) = run_mcp_server() {
1269 tracing::error!("lean-ctx: {e}");
1270 std::process::exit(1);
1271 }
1272}
1273
1274fn passthrough(command: &str) -> ! {
1275 let (shell, flag) = shell::shell_and_flag();
1276 let status = std::process::Command::new(&shell)
1277 .arg(&flag)
1278 .arg(command)
1279 .env("LEAN_CTX_ACTIVE", "1")
1280 .status()
1281 .map_or(127, |s| s.code().unwrap_or(1));
1282 std::process::exit(status);
1283}
1284
1285fn run_async<F: std::future::Future>(future: F) -> F::Output {
1286 tokio::runtime::Runtime::new()
1287 .expect("failed to create async runtime")
1288 .block_on(future)
1289}
1290
1291fn run_mcp_server() -> Result<()> {
1292 use rmcp::ServiceExt;
1293
1294 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1295
1296 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1297
1298 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1302 "mcp-startup",
1303 std::time::Duration::from_secs(3),
1304 std::time::Duration::from_secs(30),
1305 );
1306
1307 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1308 let worker_threads = parallelism.clamp(1, 4);
1309 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1310
1311 let rt = tokio::runtime::Builder::new_multi_thread()
1312 .worker_threads(worker_threads)
1313 .max_blocking_threads(max_blocking_threads)
1314 .enable_all()
1315 .build()?;
1316
1317 let server = tools::create_server();
1318 drop(startup_lock);
1319
1320 rt.block_on(async {
1321 core::logging::init_mcp_logging();
1322
1323 tracing::info!(
1324 "lean-ctx v{} MCP server starting",
1325 env!("CARGO_PKG_VERSION")
1326 );
1327
1328 let transport =
1329 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1330 let service = match server.serve(transport).await {
1331 Ok(s) => s,
1332 Err(e) => {
1333 let msg = e.to_string();
1334 if msg.contains("expect initialized")
1335 || msg.contains("context canceled")
1336 || msg.contains("broken pipe")
1337 {
1338 tracing::debug!("Client disconnected before init: {msg}");
1339 return Ok(());
1340 }
1341 return Err(e.into());
1342 }
1343 };
1344 match service.waiting().await {
1345 Ok(reason) => {
1346 tracing::info!("MCP server stopped: {reason:?}");
1347 }
1348 Err(e) => {
1349 let msg = e.to_string();
1350 if msg.contains("broken pipe")
1351 || msg.contains("connection reset")
1352 || msg.contains("context canceled")
1353 {
1354 tracing::info!("MCP server: transport closed ({msg})");
1355 } else {
1356 tracing::error!("MCP server error: {msg}");
1357 }
1358 }
1359 }
1360
1361 core::stats::flush();
1362 core::heatmap::flush();
1363 core::mode_predictor::ModePredictor::flush();
1364 core::feedback::FeedbackStore::flush();
1365
1366 Ok(())
1367 })
1368}
1369
1370fn print_help() {
1371 println!(
1372 "lean-ctx {version} — Context Runtime for AI Agents
1373
137495+ compression patterns | 57 MCP tools | Context Continuity Protocol
1375
1376USAGE:
1377 lean-ctx Start MCP server (stdio)
1378 lean-ctx serve Start MCP server (Streamable HTTP)
1379 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1380 lean-ctx serve --stop Stop running daemon
1381 lean-ctx serve --status Show daemon status
1382 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1383 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1384 lean-ctx -c --raw \"command\" Execute without compression (full output)
1385 lean-ctx exec \"command\" Same as -c
1386 lean-ctx shell Interactive shell with compression
1387
1388COMMANDS:
1389 gain Visual dashboard (colors, bars, sparklines, USD)
1390 gain --live Live mode: auto-refreshes every 1s in-place
1391 gain --graph 30-day savings chart
1392 gain --daily Bordered day-by-day table with USD
1393 gain --json Raw JSON export of all stats
1394 token-report [--json] Token + memory report (project + session + CEP)
1395 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1396 index <status|build|build-full|watch> Codebase index utilities
1397 cep CEP impact report (score trends, cache, modes)
1398 watch Live TUI dashboard (real-time event stream)
1399 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1400 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1401 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1402 proxy status Show proxy statistics
1403 cache [list|clear|stats] Show/manage file read cache
1404 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1405 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1406 benchmark run [path] [--json] Run real benchmark on project files
1407 benchmark report [path] Generate shareable Markdown report
1408 cheatsheet Command cheat sheet & workflow quick reference
1409 setup One-command setup: shell + editor + verify
1410 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1411 bootstrap Non-interactive setup + fix (zero-config)
1412 status [--json] Show setup + MCP + rules status
1413 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1414 init --agent <name> Configure MCP for specific editor/agent
1415 read <file> [-m mode] Read file with compression
1416 diff <file1> <file2> Compressed file diff
1417 grep <pattern> [path] Search with compressed output
1418 find <pattern> [path] Find files with compressed output
1419 ls [path] Directory listing with compression
1420 deps [path] Show project dependencies
1421 discover Find uncompressed commands in shell history
1422 ghost [--json] Ghost Token report: find hidden token waste
1423 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1424 session Show adoption statistics
1425 session task <desc> Set current task
1426 session finding <summary> Record a finding
1427 session save Save current session
1428 session load [id] Load session (latest if no ID)
1429 knowledge remember <value> --category <c> --key <k> Store a fact
1430 knowledge recall [query] [--category <c>] Retrieve facts
1431 knowledge search <query> Cross-project knowledge search
1432 knowledge status Knowledge base summary
1433 overview [task] Project overview (task-contextualized if given)
1434 compress [--signatures] Context compression checkpoint
1435 config Show/edit configuration (~/.lean-ctx/config.toml)
1436 profile [list|show|diff|create|set] Manage context profiles
1437 theme [list|set|export|import] Customize terminal colors and themes
1438 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1439 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1440 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1441 update [--check] Self-update lean-ctx binary from GitHub Releases
1442 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1443 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1444 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1445 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1446 uninstall Remove shell hook, MCP configs, and data directory
1447
1448SHELL HOOK PATTERNS (95+):
1449 git status, log, diff, add, commit, push, pull, fetch, clone,
1450 branch, checkout, switch, merge, stash, tag, reset, remote
1451 docker build, ps, images, logs, compose, exec, network
1452 npm/pnpm install, test, run, list, outdated, audit
1453 cargo build, test, check, clippy
1454 gh pr list/view/create, issue list/view, run list/view
1455 kubectl get pods/services/deployments, logs, describe, apply
1456 python pip install/list/outdated, ruff check/format, poetry, uv
1457 linters eslint, biome, prettier, golangci-lint
1458 builds tsc, next build, vite build
1459 ruby rubocop, bundle install/update, rake test, rails test
1460 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1461 iac terraform, make, maven, gradle, dotnet, flutter, dart
1462 utils curl, grep/rg, find, ls, wget, env
1463 data JSON schema extraction, log deduplication
1464
1465READ MODES:
1466 auto Auto-select optimal mode (default)
1467 full Full content (cached re-reads = 13 tokens)
1468 map Dependency graph + API signatures
1469 signatures tree-sitter AST extraction (18 languages)
1470 task Task-relevant filtering (requires ctx_session task)
1471 reference One-line reference stub (cheap cache key)
1472 aggressive Syntax-stripped content
1473 entropy Shannon entropy filtered
1474 diff Changed lines only
1475 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1476
1477ENVIRONMENT:
1478 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1479 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1480 LEAN_CTX_RAW=1 Same as --raw for current command
1481 LEAN_CTX_AUTONOMY=false Disable autonomous features
1482 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1483
1484OPTIONS:
1485 --version, -V Show version
1486 --help, -h Show this help
1487
1488EXAMPLES:
1489 lean-ctx -c \"git status\" Compressed git output
1490 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1491 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1492 lean-ctx gain Visual terminal dashboard
1493 lean-ctx gain --live Live auto-updating terminal dashboard
1494 lean-ctx gain --graph 30-day savings chart
1495 lean-ctx gain --daily Day-by-day breakdown with USD
1496 lean-ctx token-report --json Machine-readable token + memory report
1497 lean-ctx dashboard Open web dashboard at localhost:3333
1498 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1499 lean-ctx gain --wrapped Wrapped report card (recommended)
1500 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1501 lean-ctx sessions list List all CCP sessions
1502 lean-ctx sessions show Show latest session state
1503 lean-ctx discover Find missed savings in shell history
1504 lean-ctx setup One-command setup (shell + editors + verify)
1505 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1506 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1507 lean-ctx bootstrap --json Machine-readable bootstrap report
1508 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1509 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1510 lean-ctx-off Disable all shell aliases
1511 lean-ctx-mode track Track mode: full output, stats recorded (default)
1512 lean-ctx-mode compress Compress mode: all output compressed (power users)
1513 lean-ctx-mode off Same as lean-ctx-off
1514 lean-ctx-status Show whether compression is active
1515 lean-ctx init --agent pi Install Pi Coding Agent extension
1516 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1517 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1518 lean-ctx doctor --fix --json Repair + machine-readable report
1519 lean-ctx status --json Machine-readable current status
1520 lean-ctx session task \"implement auth\"
1521 lean-ctx session finding \"auth.rs:42 — missing validation\"
1522 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1523 lean-ctx knowledge recall \"authentication\"
1524 lean-ctx knowledge search \"database migration\"
1525 lean-ctx overview \"refactor auth module\"
1526 lean-ctx compress --signatures
1527 lean-ctx read src/main.rs -m map
1528 lean-ctx grep \"pub fn\" src/
1529 lean-ctx deps .
1530
1531CLOUD:
1532 cloud status Show cloud connection status
1533 login <email> Log into existing LeanCTX Cloud account
1534 register <email> Create a new LeanCTX Cloud account
1535 forgot-password <email> Send password reset email
1536 sync Upload local stats to cloud dashboard
1537 contribute Share anonymized compression data
1538
1539TROUBLESHOOTING:
1540 Commands broken? lean-ctx-off (fixes current session)
1541 Permanent fix? lean-ctx uninstall (removes all hooks)
1542 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1543 Binary missing? Aliases auto-fallback to original commands (safe)
1544 Preview init? lean-ctx init --global --dry-run
1545
1546WEBSITE: https://leanctx.com
1547GITHUB: https://github.com/yvgude/lean-ctx
1548",
1549 version = env!("CARGO_PKG_VERSION"),
1550 );
1551}