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