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