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