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_else(crate::proxy_setup::default_port);
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_else(crate::proxy_setup::default_port);
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_else(crate::proxy_setup::default_port);
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 let home = dirs::home_dir().unwrap_or_default();
867 if crate::proxy_setup::has_stale_proxy_url(&home) {
868 println!();
869 println!(" \x1b[33m⚠ WARNING: Claude Code ANTHROPIC_BASE_URL points to the local proxy,\x1b[0m");
870 println!(" \x1b[33m but proxy is not enabled. This causes 401 auth failures.\x1b[0m");
871 println!(" Fix: lean-ctx proxy cleanup (remove stale URL)");
872 println!(" lean-ctx proxy enable (enable the proxy)");
873 }
874 }
875 }
876 "enable" => {
877 let force = rest.iter().any(|a| a == "--force");
878 let mut cfg = crate::core::config::Config::load();
879 cfg.proxy_enabled = Some(true);
880 let _ = cfg.save();
881
882 let port = crate::proxy_setup::default_port();
883 crate::proxy_autostart::install(port, false);
884 std::thread::sleep(std::time::Duration::from_millis(500));
885
886 let home = dirs::home_dir().unwrap_or_default();
887 crate::proxy_setup::install_proxy_env_unchecked(
888 &home, port, false, force,
889 );
890 println!("\x1b[32m✓\x1b[0m Proxy enabled on port {port}. LLM requests will be compressed.");
891 }
892 "disable" => {
893 let mut cfg = crate::core::config::Config::load();
894 cfg.proxy_enabled = Some(false);
895 let _ = cfg.save();
896
897 crate::proxy_autostart::uninstall(false);
898 let home = dirs::home_dir().unwrap_or_default();
899 crate::proxy_setup::uninstall_proxy_env(&home, false);
900
901 println!(
902 "\x1b[32m✓\x1b[0m Proxy disabled. Original endpoint restored."
903 );
904 println!(" Re-enable anytime: lean-ctx proxy enable");
905 }
906 "cleanup" => {
907 let home = dirs::home_dir().unwrap_or_default();
908 let removed = crate::proxy_setup::cleanup_stale_proxy_env(&home);
909 if removed > 0 {
910 println!(
911 "\x1b[32m✓\x1b[0m Cleaned up {removed} stale proxy URL(s)."
912 );
913 println!(" Restart your AI tool for changes to take effect.");
914 } else {
915 println!(" No stale proxy URLs found. Nothing to clean up.");
916 }
917 }
918 _ => {
919 println!("Usage: lean-ctx proxy <start|stop|status|enable|disable|cleanup> [--port=4444]");
920 }
921 }
922 return;
923 }
924 #[cfg(not(feature = "http-server"))]
925 {
926 eprintln!("lean-ctx proxy is not available in this build");
927 std::process::exit(1);
928 }
929 }
930 "init" => {
931 super::cmd_init(&rest);
932 return;
933 }
934 "setup" => {
935 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
936 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
937 let fix = rest.iter().any(|a| a == "--fix");
938 let json = rest.iter().any(|a| a == "--json");
939 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
940
941 if non_interactive || fix || json || yes {
942 let opts = setup::SetupOptions {
943 non_interactive,
944 yes,
945 fix,
946 json,
947 no_auto_approve,
948 ..Default::default()
949 };
950 match setup::run_setup_with_options(opts) {
951 Ok(report) => {
952 if json {
953 println!(
954 "{}",
955 serde_json::to_string_pretty(&report)
956 .unwrap_or_else(|_| "{}".to_string())
957 );
958 }
959 if !report.success {
960 std::process::exit(1);
961 }
962 }
963 Err(e) => {
964 eprintln!("{e}");
965 std::process::exit(1);
966 }
967 }
968 } else {
969 setup::run_setup();
970 }
971 return;
972 }
973 "install" => {
974 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
975 let json = rest.iter().any(|a| a == "--json");
976 if !repair {
977 eprintln!("Usage: lean-ctx install --repair [--json]");
978 std::process::exit(1);
979 }
980 let opts = setup::SetupOptions {
981 non_interactive: true,
982 yes: true,
983 fix: true,
984 json,
985 ..Default::default()
986 };
987 match setup::run_setup_with_options(opts) {
988 Ok(report) => {
989 if json {
990 println!(
991 "{}",
992 serde_json::to_string_pretty(&report)
993 .unwrap_or_else(|_| "{}".to_string())
994 );
995 }
996 if !report.success {
997 std::process::exit(1);
998 }
999 }
1000 Err(e) => {
1001 eprintln!("{e}");
1002 std::process::exit(1);
1003 }
1004 }
1005 return;
1006 }
1007 "bootstrap" => {
1008 let json = rest.iter().any(|a| a == "--json");
1009 let opts = setup::SetupOptions {
1010 non_interactive: true,
1011 yes: true,
1012 fix: true,
1013 json,
1014 ..Default::default()
1015 };
1016 match setup::run_setup_with_options(opts) {
1017 Ok(report) => {
1018 if json {
1019 println!(
1020 "{}",
1021 serde_json::to_string_pretty(&report)
1022 .unwrap_or_else(|_| "{}".to_string())
1023 );
1024 }
1025 if !report.success {
1026 std::process::exit(1);
1027 }
1028 }
1029 Err(e) => {
1030 eprintln!("{e}");
1031 std::process::exit(1);
1032 }
1033 }
1034 return;
1035 }
1036 "status" => {
1037 let code = status::run_cli(&rest);
1038 if code != 0 {
1039 std::process::exit(code);
1040 }
1041 return;
1042 }
1043 "read" => {
1044 super::cmd_read(&rest);
1045 core::stats::flush();
1046 return;
1047 }
1048 "diff" => {
1049 super::cmd_diff(&rest);
1050 core::stats::flush();
1051 return;
1052 }
1053 "grep" => {
1054 super::cmd_grep(&rest);
1055 core::stats::flush();
1056 return;
1057 }
1058 "find" => {
1059 super::cmd_find(&rest);
1060 core::stats::flush();
1061 return;
1062 }
1063 "ls" => {
1064 super::cmd_ls(&rest);
1065 core::stats::flush();
1066 return;
1067 }
1068 "deps" => {
1069 super::cmd_deps(&rest);
1070 core::stats::flush();
1071 return;
1072 }
1073 "discover" => {
1074 super::cmd_discover(&rest);
1075 return;
1076 }
1077 "ghost" => {
1078 super::cmd_ghost(&rest);
1079 return;
1080 }
1081 "filter" => {
1082 super::cmd_filter(&rest);
1083 return;
1084 }
1085 "heatmap" => {
1086 heatmap::cmd_heatmap(&rest);
1087 return;
1088 }
1089 "graph" => {
1090 let sub = rest.first().map_or("build", std::string::String::as_str);
1091 match sub {
1092 "build" => {
1093 let root = rest.get(1).cloned().or_else(|| {
1094 std::env::current_dir()
1095 .ok()
1096 .map(|p| p.to_string_lossy().to_string())
1097 });
1098 let root = root.unwrap_or_else(|| ".".to_string());
1099 let index = core::graph_index::load_or_build(&root);
1100 println!(
1101 "Graph built: {} files, {} edges",
1102 index.files.len(),
1103 index.edges.len()
1104 );
1105 }
1106 "export-html" => {
1107 let mut root: Option<String> = None;
1108 let mut out: Option<String> = None;
1109 let mut max_nodes: usize = 2500;
1110
1111 let args = &rest[1..];
1112 let mut i = 0usize;
1113 while i < args.len() {
1114 let a = args[i].as_str();
1115 if let Some(v) = a.strip_prefix("--root=") {
1116 root = Some(v.to_string());
1117 } else if a == "--root" {
1118 root = args.get(i + 1).cloned();
1119 i += 1;
1120 } else if let Some(v) = a.strip_prefix("--out=") {
1121 out = Some(v.to_string());
1122 } else if a == "--out" {
1123 out = args.get(i + 1).cloned();
1124 i += 1;
1125 } else if let Some(v) = a.strip_prefix("--max-nodes=") {
1126 max_nodes = v.parse::<usize>().unwrap_or(0);
1127 } else if a == "--max-nodes" {
1128 let v = args.get(i + 1).map_or("", String::as_str);
1129 max_nodes = v.parse::<usize>().unwrap_or(0);
1130 i += 1;
1131 }
1132 i += 1;
1133 }
1134
1135 let root = root
1136 .or_else(|| {
1137 std::env::current_dir()
1138 .ok()
1139 .map(|p| p.to_string_lossy().to_string())
1140 })
1141 .unwrap_or_else(|| ".".to_string());
1142 let Some(out) = out else {
1143 eprintln!("Usage: lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]");
1144 std::process::exit(1);
1145 };
1146 if max_nodes == 0 {
1147 eprintln!("--max-nodes must be >= 1");
1148 std::process::exit(1);
1149 }
1150
1151 core::graph_export::export_graph_html(
1152 &root,
1153 std::path::Path::new(&out),
1154 max_nodes,
1155 )
1156 .unwrap_or_else(|e| {
1157 eprintln!("graph export failed: {e}");
1158 std::process::exit(1);
1159 });
1160 println!("{out}");
1161 }
1162 _ => {
1163 eprintln!(
1164 "Usage:\n lean-ctx graph build [path]\n lean-ctx graph export-html --out <path> [--root <path>] [--max-nodes <n>]"
1165 );
1166 std::process::exit(1);
1167 }
1168 }
1169 return;
1170 }
1171 "smells" => {
1172 let action = rest.first().map_or("summary", String::as_str);
1173 let rule = rest.iter().enumerate().find_map(|(i, a)| {
1174 if let Some(v) = a.strip_prefix("--rule=") {
1175 return Some(v.to_string());
1176 }
1177 if a == "--rule" {
1178 return rest.get(i + 1).cloned();
1179 }
1180 None
1181 });
1182 let path = rest.iter().enumerate().find_map(|(i, a)| {
1183 if let Some(v) = a.strip_prefix("--path=") {
1184 return Some(v.to_string());
1185 }
1186 if a == "--path" {
1187 return rest.get(i + 1).cloned();
1188 }
1189 None
1190 });
1191 let root = rest
1192 .iter()
1193 .enumerate()
1194 .find_map(|(i, a)| {
1195 if let Some(v) = a.strip_prefix("--root=") {
1196 return Some(v.to_string());
1197 }
1198 if a == "--root" {
1199 return rest.get(i + 1).cloned();
1200 }
1201 None
1202 })
1203 .or_else(|| {
1204 std::env::current_dir()
1205 .ok()
1206 .map(|p| p.to_string_lossy().to_string())
1207 })
1208 .unwrap_or_else(|| ".".to_string());
1209 let fmt = if rest.iter().any(|a| a == "--json") {
1210 Some("json")
1211 } else {
1212 None
1213 };
1214 println!(
1215 "{}",
1216 tools::ctx_smells::handle(action, rule.as_deref(), path.as_deref(), &root, fmt)
1217 );
1218 return;
1219 }
1220 "session" => {
1221 super::cmd_session_action(&rest);
1222 return;
1223 }
1224 "ledger" => {
1225 super::cmd_ledger(&rest);
1226 return;
1227 }
1228 "control" | "context-control" => {
1229 super::cmd_control(&rest);
1230 return;
1231 }
1232 "plan" | "context-plan" => {
1233 super::cmd_plan(&rest);
1234 return;
1235 }
1236 "compile" | "context-compile" => {
1237 super::cmd_compile(&rest);
1238 return;
1239 }
1240 "knowledge" => {
1241 super::cmd_knowledge(&rest);
1242 return;
1243 }
1244 "overview" => {
1245 super::cmd_overview(&rest);
1246 return;
1247 }
1248 "compress" => {
1249 super::cmd_compress(&rest);
1250 return;
1251 }
1252 "wrapped" => {
1253 super::cmd_wrapped(&rest);
1254 return;
1255 }
1256 "sessions" => {
1257 super::cmd_sessions(&rest);
1258 return;
1259 }
1260 "benchmark" => {
1261 super::cmd_benchmark(&rest);
1262 return;
1263 }
1264 "profile" => {
1265 super::cmd_profile(&rest);
1266 return;
1267 }
1268 "config" => {
1269 super::cmd_config(&rest);
1270 return;
1271 }
1272 "stats" => {
1273 super::cmd_stats(&rest);
1274 return;
1275 }
1276 "cache" => {
1277 super::cmd_cache(&rest);
1278 return;
1279 }
1280 "theme" => {
1281 super::cmd_theme(&rest);
1282 return;
1283 }
1284 "tee" => {
1285 super::cmd_tee(&rest);
1286 return;
1287 }
1288 "terse" | "compression" => {
1289 super::cmd_compression(&rest);
1290 return;
1291 }
1292 "slow-log" => {
1293 super::cmd_slow_log(&rest);
1294 return;
1295 }
1296 "update" | "--self-update" => {
1297 core::updater::run(&rest);
1298 return;
1299 }
1300 "restart" => {
1301 cmd_restart();
1302 return;
1303 }
1304 "stop" => {
1305 cmd_stop();
1306 return;
1307 }
1308 "dev-install" => {
1309 cmd_dev_install();
1310 return;
1311 }
1312 "doctor" => {
1313 let code = doctor::run_cli(&rest);
1314 if code != 0 {
1315 std::process::exit(code);
1316 }
1317 return;
1318 }
1319 "harden" => {
1320 super::harden::run(&rest);
1321 return;
1322 }
1323 "export-rules" => {
1324 super::export_rules::run(&rest);
1325 return;
1326 }
1327 "gotchas" | "bugs" => {
1328 super::cloud::cmd_gotchas(&rest);
1329 return;
1330 }
1331 "learn" => {
1332 super::cmd_learn(&rest);
1333 return;
1334 }
1335 "buddy" | "pet" => {
1336 super::cloud::cmd_buddy(&rest);
1337 return;
1338 }
1339 "hook" => {
1340 hook_handlers::mark_hook_environment();
1341 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1342 let action = rest.first().map_or("help", std::string::String::as_str);
1343 match action {
1344 "rewrite" => hook_handlers::handle_rewrite(),
1345 "redirect" => hook_handlers::handle_redirect(),
1346 "observe" => hook_handlers::handle_observe(),
1347 "copilot" => hook_handlers::handle_copilot(),
1348 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1349 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1350 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1351 _ => {
1352 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1353 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1354 std::process::exit(1);
1355 }
1356 }
1357 return;
1358 }
1359 "report-issue" | "report" => {
1360 report::run(&rest);
1361 return;
1362 }
1363 "uninstall" => {
1364 let dry_run = rest.iter().any(|a| a == "--dry-run");
1365 uninstall::run(dry_run);
1366 return;
1367 }
1368 "bypass" => {
1369 if rest.is_empty() {
1370 eprintln!("Usage: lean-ctx bypass \"command\"");
1371 eprintln!("Runs the command with zero compression (raw passthrough).");
1372 std::process::exit(1);
1373 }
1374 let command = if rest.len() == 1 {
1375 rest[0].clone()
1376 } else {
1377 shell::join_command(&args[2..])
1378 };
1379 std::env::set_var("LEAN_CTX_RAW", "1");
1380 let code = shell::exec(&command);
1381 std::process::exit(code);
1382 }
1383 "safety-levels" | "safety" => {
1384 println!("{}", core::compression_safety::format_safety_table());
1385 return;
1386 }
1387 "cheat" | "cheatsheet" | "cheat-sheet" => {
1388 super::cmd_cheatsheet();
1389 return;
1390 }
1391 "login" => {
1392 super::cloud::cmd_login(&rest);
1393 return;
1394 }
1395 "register" => {
1396 super::cloud::cmd_register(&rest);
1397 return;
1398 }
1399 "forgot-password" => {
1400 super::cloud::cmd_forgot_password(&rest);
1401 return;
1402 }
1403 "sync" => {
1404 super::cloud::cmd_sync();
1405 return;
1406 }
1407 "contribute" => {
1408 super::cloud::cmd_contribute();
1409 return;
1410 }
1411 "cloud" => {
1412 super::cloud::cmd_cloud(&rest);
1413 return;
1414 }
1415 "upgrade" => {
1416 super::cloud::cmd_upgrade();
1417 return;
1418 }
1419 "--version" | "-V" => {
1420 println!("{}", core::integrity::origin_line());
1421 return;
1422 }
1423 "--help" | "-h" => {
1424 print_help();
1425 return;
1426 }
1427 "mcp" => {}
1428 _ => {
1429 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1430 print_help();
1431 std::process::exit(1);
1432 }
1433 }
1434 }
1435
1436 if let Err(e) = run_mcp_server() {
1437 tracing::error!("lean-ctx: {e}");
1438 std::process::exit(1);
1439 }
1440}
1441
1442fn passthrough(command: &str) -> ! {
1443 let (shell, flag) = shell::shell_and_flag();
1444 let status = std::process::Command::new(&shell)
1445 .arg(&flag)
1446 .arg(command)
1447 .env("LEAN_CTX_ACTIVE", "1")
1448 .status()
1449 .map_or(127, |s| s.code().unwrap_or(1));
1450 std::process::exit(status);
1451}
1452
1453fn run_async<F: std::future::Future>(future: F) -> F::Output {
1454 tokio::runtime::Runtime::new()
1455 .expect("failed to create async runtime")
1456 .block_on(future)
1457}
1458
1459fn run_mcp_server() -> Result<()> {
1460 use rmcp::ServiceExt;
1461
1462 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1463
1464 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1465
1466 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1472 "mcp-startup",
1473 std::time::Duration::from_secs(3),
1474 std::time::Duration::from_secs(30),
1475 );
1476
1477 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1478 let worker_threads = resolve_worker_threads(parallelism);
1479 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1480
1481 let rt = tokio::runtime::Builder::new_multi_thread()
1482 .worker_threads(worker_threads)
1483 .max_blocking_threads(max_blocking_threads)
1484 .enable_all()
1485 .build()?;
1486
1487 let server = tools::create_server();
1488 drop(startup_lock);
1489
1490 spawn_proxy_if_needed();
1492
1493 rt.block_on(async {
1494 core::logging::init_mcp_logging();
1495 core::protocol::set_mcp_context(true);
1496
1497 tracing::info!(
1498 "lean-ctx v{} MCP server starting",
1499 env!("CARGO_PKG_VERSION")
1500 );
1501
1502 let transport =
1503 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1504 let server_handle = server.clone();
1505 let service = match server.serve(transport).await {
1506 Ok(s) => s,
1507 Err(e) => {
1508 let msg = e.to_string();
1509 if msg.contains("expect initialized")
1510 || msg.contains("context canceled")
1511 || msg.contains("broken pipe")
1512 {
1513 tracing::debug!("Client disconnected before init: {msg}");
1514 return Ok(());
1515 }
1516 return Err(e.into());
1517 }
1518 };
1519 match service.waiting().await {
1520 Ok(reason) => {
1521 tracing::info!("MCP server stopped: {reason:?}");
1522 }
1523 Err(e) => {
1524 let msg = e.to_string();
1525 if msg.contains("broken pipe")
1526 || msg.contains("connection reset")
1527 || msg.contains("context canceled")
1528 {
1529 tracing::info!("MCP server: transport closed ({msg})");
1530 } else {
1531 tracing::error!("MCP server error: {msg}");
1532 }
1533 }
1534 }
1535
1536 server_handle.shutdown().await;
1537
1538 core::stats::flush();
1539 core::heatmap::flush();
1540 core::mode_predictor::ModePredictor::flush();
1541 core::feedback::FeedbackStore::flush();
1542
1543 Ok(())
1544 })
1545}
1546
1547fn print_help() {
1548 println!(
1549 "lean-ctx {version} — Context Runtime for AI Agents
1550
155160+ compression patterns | 61 MCP tools | 10 read modes | Context Continuity Protocol
1552
1553USAGE:
1554 lean-ctx Start MCP server (stdio)
1555 lean-ctx serve Start MCP server (Streamable HTTP)
1556 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1557 lean-ctx serve --stop Stop running daemon
1558 lean-ctx serve --status Show daemon status
1559 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1560 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1561 lean-ctx -c --raw \"command\" Execute without compression (full output)
1562 lean-ctx exec \"command\" Same as -c
1563 lean-ctx shell Interactive shell with compression
1564
1565COMMANDS:
1566 gain Visual dashboard (colors, bars, sparklines, USD)
1567 gain --live Live mode: auto-refreshes every 1s in-place
1568 gain --graph 30-day savings chart
1569 gain --daily Bordered day-by-day table with USD
1570 gain --json Raw JSON export of all stats
1571 token-report [--json] Token + memory report (project + session + CEP)
1572 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1573 index <status|build|build-full|watch> Codebase index utilities
1574 cep CEP impact report (score trends, cache, modes)
1575 watch Live TUI dashboard (real-time event stream)
1576 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1577 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1578 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1579 proxy status Show proxy statistics
1580 cache [list|clear|stats] Show/manage file read cache
1581 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1582 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1583 benchmark run [path] [--json] Run real benchmark on project files
1584 benchmark report [path] Generate shareable Markdown report
1585 cheatsheet Command cheat sheet & workflow quick reference
1586 setup One-command setup: shell + editor + verify
1587 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1588 bootstrap Non-interactive setup + fix (zero-config)
1589 status [--json] Show setup + MCP + rules status
1590 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1591 init --agent <name> Configure MCP for specific editor/agent
1592 read <file> [-m mode] Read file with compression
1593 diff <file1> <file2> Compressed file diff
1594 grep <pattern> [path] Search with compressed output
1595 find <pattern> [path] Find files with compressed output
1596 ls [path] Directory listing with compression
1597 deps [path] Show project dependencies
1598 discover Find uncompressed commands in shell history
1599 ghost [--json] Ghost Token report: find hidden token waste
1600 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1601 session Show adoption statistics
1602 session task <desc> Set current task
1603 session finding <summary> Record a finding
1604 session save Save current session
1605 session load [id] Load session (latest if no ID)
1606 knowledge remember <value> --category <c> --key <k> Store a fact
1607 knowledge recall [query] [--category <c>] Retrieve facts
1608 knowledge search <query> Cross-project knowledge search
1609 knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
1610 knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
1611 knowledge remove --category <c> --key <k> Remove a fact
1612 knowledge status Knowledge base summary
1613 overview [task] Project overview (task-contextualized if given)
1614 compress [--signatures] Context compression checkpoint
1615 config Show/edit configuration (~/.lean-ctx/config.toml)
1616 profile [list|show|diff|create|set] Manage context profiles
1617 theme [list|set|export|import] Customize terminal colors and themes
1618 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1619 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1620 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1621 update [--check] Self-update lean-ctx binary from GitHub Releases
1622 stop Stop ALL lean-ctx processes (daemon, proxy, orphans)
1623 restart Restart daemon (applies config.toml changes)
1624 dev-install Build release + atomic install + restart (for development)
1625 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1626 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1627 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1628 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1629 smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1630 Code smell detection (Property Graph, 8 rules)
1631 control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1632 plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
1633 compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1634 uninstall Remove shell hook, MCP configs, and data directory
1635
1636SHELL HOOK PATTERNS (95+):
1637 git status, log, diff, add, commit, push, pull, fetch, clone,
1638 branch, checkout, switch, merge, stash, tag, reset, remote
1639 docker build, ps, images, logs, compose, exec, network
1640 npm/pnpm install, test, run, list, outdated, audit
1641 cargo build, test, check, clippy
1642 gh pr list/view/create, issue list/view, run list/view
1643 kubectl get pods/services/deployments, logs, describe, apply
1644 python pip install/list/outdated, ruff check/format, poetry, uv
1645 linters eslint, biome, prettier, golangci-lint
1646 builds tsc, next build, vite build
1647 ruby rubocop, bundle install/update, rake test, rails test
1648 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1649 iac terraform, make, maven, gradle, dotnet, flutter, dart
1650 utils curl, grep/rg, find, ls, wget, env
1651 data JSON schema extraction, log deduplication
1652
1653READ MODES:
1654 auto Auto-select optimal mode (default)
1655 full Full content (cached re-reads = 13 tokens)
1656 map Dependency graph + API signatures
1657 signatures tree-sitter AST extraction (18 languages)
1658 task Task-relevant filtering (requires ctx_session task)
1659 reference One-line reference stub (cheap cache key)
1660 aggressive Syntax-stripped content
1661 entropy Shannon entropy filtered
1662 diff Changed lines only
1663 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1664
1665ENVIRONMENT:
1666 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1667 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1668 LEAN_CTX_RAW=1 Same as --raw for current command
1669 LEAN_CTX_AUTONOMY=false Disable autonomous features
1670 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1671
1672OPTIONS:
1673 --version, -V Show version
1674 --help, -h Show this help
1675
1676EXAMPLES:
1677 lean-ctx -c \"git status\" Compressed git output
1678 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1679 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1680 lean-ctx gain Visual terminal dashboard
1681 lean-ctx gain --live Live auto-updating terminal dashboard
1682 lean-ctx gain --graph 30-day savings chart
1683 lean-ctx gain --daily Day-by-day breakdown with USD
1684 lean-ctx token-report --json Machine-readable token + memory report
1685 lean-ctx dashboard Open web dashboard at localhost:3333
1686 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1687 lean-ctx gain --wrapped Wrapped report card (recommended)
1688 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1689 lean-ctx sessions list List all CCP sessions
1690 lean-ctx sessions show Show latest session state
1691 lean-ctx discover Find missed savings in shell history
1692 lean-ctx setup One-command setup (shell + editors + verify)
1693 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1694 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1695 lean-ctx bootstrap --json Machine-readable bootstrap report
1696 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1697 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1698 lean-ctx-off Disable all shell aliases
1699 lean-ctx-mode track Track mode: full output, stats recorded (default)
1700 lean-ctx-mode compress Compress mode: all output compressed (power users)
1701 lean-ctx-mode off Same as lean-ctx-off
1702 lean-ctx-status Show whether compression is active
1703 lean-ctx init --agent pi Install Pi Coding Agent extension
1704 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1705 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1706 lean-ctx doctor --fix --json Repair + machine-readable report
1707 lean-ctx status --json Machine-readable current status
1708 lean-ctx session task \"implement auth\"
1709 lean-ctx session finding \"auth.rs:42 — missing validation\"
1710 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1711 lean-ctx knowledge recall \"authentication\"
1712 lean-ctx knowledge search \"database migration\"
1713 lean-ctx overview \"refactor auth module\"
1714 lean-ctx compress --signatures
1715 lean-ctx read src/main.rs -m map
1716 lean-ctx grep \"pub fn\" src/
1717 lean-ctx deps .
1718
1719CLOUD:
1720 cloud status Show cloud connection status
1721 login <email> Log into existing LeanCTX Cloud account
1722 register <email> Create a new LeanCTX Cloud account
1723 forgot-password <email> Send password reset email
1724 sync Upload local stats to cloud dashboard
1725 contribute Share anonymized compression data
1726
1727TROUBLESHOOTING:
1728 Commands broken? lean-ctx-off (fixes current session)
1729 Permanent fix? lean-ctx uninstall (removes all hooks)
1730 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1731 Binary missing? Aliases auto-fallback to original commands (safe)
1732 Preview init? lean-ctx init --global --dry-run
1733
1734WEBSITE: https://leanctx.com
1735GITHUB: https://github.com/yvgude/lean-ctx
1736",
1737 version = env!("CARGO_PKG_VERSION"),
1738 );
1739}
1740
1741fn cmd_stop() {
1742 use crate::daemon;
1743 use crate::ipc;
1744
1745 eprintln!("Stopping all lean-ctx processes…");
1746
1747 crate::proxy_autostart::stop();
1749 eprintln!(" Unloaded autostart (LaunchAgent/systemd).");
1750
1751 if let Err(e) = daemon::stop_daemon() {
1753 eprintln!(" Warning: daemon stop: {e}");
1754 }
1755
1756 let killed = ipc::process::kill_all_by_name("lean-ctx");
1758 if killed > 0 {
1759 eprintln!(" Sent SIGTERM to {killed} process(es).");
1760 }
1761
1762 std::thread::sleep(std::time::Duration::from_millis(500));
1763
1764 let remaining = ipc::process::find_killable_pids("lean-ctx");
1766 if !remaining.is_empty() {
1767 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
1768 for &pid in &remaining {
1769 let _ = ipc::process::force_kill(pid);
1770 }
1771 std::thread::sleep(std::time::Duration::from_millis(300));
1772 }
1773
1774 daemon::cleanup_daemon_files();
1775
1776 let final_check = ipc::process::find_killable_pids("lean-ctx");
1777 if final_check.is_empty() {
1778 eprintln!(" ✓ All lean-ctx processes stopped.");
1779 } else {
1780 eprintln!(
1781 " ✗ {} process(es) could not be killed: {:?}",
1782 final_check.len(),
1783 final_check
1784 );
1785 eprintln!(
1786 " Try: sudo kill -9 {}",
1787 final_check
1788 .iter()
1789 .map(std::string::ToString::to_string)
1790 .collect::<Vec<_>>()
1791 .join(" ")
1792 );
1793 std::process::exit(1);
1794 }
1795}
1796
1797fn cmd_restart() {
1798 use crate::daemon;
1799 use crate::ipc;
1800
1801 eprintln!("Restarting lean-ctx…");
1802
1803 crate::proxy_autostart::stop();
1805
1806 if let Err(e) = daemon::stop_daemon() {
1807 eprintln!(" Warning: daemon stop: {e}");
1808 }
1809
1810 let orphans = ipc::process::kill_all_by_name("lean-ctx");
1811 if orphans > 0 {
1812 eprintln!(" Terminated {orphans} orphan process(es).");
1813 }
1814
1815 std::thread::sleep(std::time::Duration::from_millis(500));
1816
1817 let remaining = ipc::process::find_pids_by_name("lean-ctx");
1818 if !remaining.is_empty() {
1819 eprintln!(
1820 " Force-killing {} stubborn process(es): {:?}",
1821 remaining.len(),
1822 remaining
1823 );
1824 for &pid in &remaining {
1825 let _ = ipc::process::force_kill(pid);
1826 }
1827 std::thread::sleep(std::time::Duration::from_millis(300));
1828 }
1829
1830 daemon::cleanup_daemon_files();
1831
1832 crate::proxy_autostart::start();
1834
1835 match daemon::start_daemon(&[]) {
1836 Ok(()) => eprintln!(" ✓ Daemon restarted. Config changes are now active."),
1837 Err(e) => {
1838 eprintln!(" ✗ Daemon start failed: {e}");
1839 std::process::exit(1);
1840 }
1841 }
1842}
1843
1844fn cmd_dev_install() {
1845 use crate::ipc;
1846
1847 let cargo_root = find_cargo_project_root();
1848 let Some(cargo_root) = cargo_root else {
1849 eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1850 std::process::exit(1);
1851 };
1852
1853 eprintln!("Building release binary…");
1854 let build = std::process::Command::new("cargo")
1855 .args(["build", "--release"])
1856 .current_dir(&cargo_root)
1857 .status();
1858
1859 match build {
1860 Ok(s) if s.success() => {}
1861 Ok(s) => {
1862 eprintln!(" Build failed with exit code {}", s.code().unwrap_or(-1));
1863 std::process::exit(1);
1864 }
1865 Err(e) => {
1866 eprintln!(" Build failed: {e}");
1867 std::process::exit(1);
1868 }
1869 }
1870
1871 let built_binary = cargo_root.join("target/release/lean-ctx");
1872 if !built_binary.exists() {
1873 eprintln!(
1874 " Error: Built binary not found at {}",
1875 built_binary.display()
1876 );
1877 std::process::exit(1);
1878 }
1879
1880 let install_path = resolve_install_path();
1881 eprintln!("Installing to {}…", install_path.display());
1882
1883 eprintln!(" Stopping all lean-ctx processes…");
1884 crate::proxy_autostart::stop();
1885 let _ = crate::daemon::stop_daemon();
1886 ipc::process::kill_all_by_name("lean-ctx");
1887 std::thread::sleep(std::time::Duration::from_millis(500));
1888
1889 let remaining = ipc::process::find_pids_by_name("lean-ctx");
1890 if !remaining.is_empty() {
1891 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
1892 for &pid in &remaining {
1893 let _ = ipc::process::force_kill(pid);
1894 }
1895 std::thread::sleep(std::time::Duration::from_millis(500));
1896 }
1897
1898 let old_path = install_path.with_extension("old");
1899 if install_path.exists() {
1900 if let Err(e) = std::fs::rename(&install_path, &old_path) {
1901 eprintln!(" Warning: rename existing binary: {e}");
1902 }
1903 }
1904
1905 match std::fs::copy(&built_binary, &install_path) {
1906 Ok(_) => {
1907 let _ = std::fs::remove_file(&old_path);
1908 #[cfg(unix)]
1909 {
1910 use std::os::unix::fs::PermissionsExt;
1911 let _ =
1912 std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1913 }
1914 eprintln!(" ✓ Binary installed.");
1915 }
1916 Err(e) => {
1917 eprintln!(" Error: copy failed: {e}");
1918 if old_path.exists() {
1919 let _ = std::fs::rename(&old_path, &install_path);
1920 eprintln!(" Rolled back to previous binary.");
1921 }
1922 std::process::exit(1);
1923 }
1924 }
1925
1926 let version = std::process::Command::new(&install_path)
1927 .arg("--version")
1928 .output()
1929 .map_or_else(
1930 |_| "unknown".to_string(),
1931 |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1932 );
1933
1934 eprintln!(" ✓ dev-install complete: {version}");
1935
1936 eprintln!(" Re-enabling autostart…");
1937 crate::proxy_autostart::start();
1938
1939 eprintln!(" Starting daemon…");
1940 match crate::daemon::start_daemon(&[]) {
1941 Ok(()) => {}
1942 Err(e) => eprintln!(" Warning: daemon start: {e} (will be started by editor)"),
1943 }
1944}
1945
1946fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1947 let mut dir = std::env::current_dir().ok()?;
1948 loop {
1949 if dir.join("Cargo.toml").exists() {
1950 return Some(dir);
1951 }
1952 if !dir.pop() {
1953 return None;
1954 }
1955 }
1956}
1957
1958fn resolve_install_path() -> std::path::PathBuf {
1959 if let Ok(exe) = std::env::current_exe() {
1960 if let Ok(canonical) = exe.canonicalize() {
1961 let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1962 if !is_in_cargo_target && canonical.exists() {
1963 return canonical;
1964 }
1965 }
1966 }
1967
1968 if let Ok(home) = std::env::var("HOME") {
1969 let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1970 if local_bin.parent().is_some_and(std::path::Path::exists) {
1971 return local_bin;
1972 }
1973 }
1974
1975 std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1976}
1977
1978fn spawn_proxy_if_needed() {
1979 use std::net::TcpStream;
1980 use std::time::Duration;
1981
1982 let cfg = core::config::Config::load();
1983 if cfg.proxy_enabled != Some(true) {
1984 return;
1985 }
1986
1987 let port = crate::proxy_setup::default_port();
1988 let already_running = TcpStream::connect_timeout(
1989 &format!("127.0.0.1:{port}").parse().unwrap(),
1990 Duration::from_millis(200),
1991 )
1992 .is_ok();
1993
1994 if already_running {
1995 tracing::debug!("proxy already running on port {port}");
1996 return;
1997 }
1998
1999 let binary = core::portable_binary::resolve_portable_binary();
2000
2001 match std::process::Command::new(&binary)
2002 .args(["proxy", "start", &format!("--port={port}")])
2003 .stdin(std::process::Stdio::null())
2004 .stdout(std::process::Stdio::null())
2005 .stderr(std::process::Stdio::null())
2006 .spawn()
2007 {
2008 Ok(_) => tracing::info!("auto-started proxy on port {port}"),
2009 Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
2010 }
2011}
2012
2013fn resolve_worker_threads(parallelism: usize) -> usize {
2014 std::env::var("LEAN_CTX_WORKER_THREADS")
2015 .ok()
2016 .and_then(|v| v.parse::<usize>().ok())
2017 .unwrap_or_else(|| parallelism.clamp(1, 4))
2018}
2019
2020#[cfg(test)]
2021mod tests {
2022 use super::*;
2023 use serial_test::serial;
2024
2025 #[test]
2026 #[serial]
2027 fn worker_threads_default_clamps_low() {
2028 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2029 assert_eq!(resolve_worker_threads(1), 1);
2030 }
2031
2032 #[test]
2033 #[serial]
2034 fn worker_threads_default_clamps_high() {
2035 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2036 assert_eq!(resolve_worker_threads(32), 4);
2037 }
2038
2039 #[test]
2040 #[serial]
2041 fn worker_threads_default_passthrough() {
2042 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2043 assert_eq!(resolve_worker_threads(3), 3);
2044 }
2045
2046 #[test]
2047 #[serial]
2048 fn worker_threads_env_override() {
2049 std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
2050 assert_eq!(resolve_worker_threads(2), 12);
2051 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2052 }
2053
2054 #[test]
2055 #[serial]
2056 fn worker_threads_env_invalid_falls_back() {
2057 std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
2058 assert_eq!(resolve_worker_threads(3), 3);
2059 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2060 }
2061}