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