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