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 "ledger" => {
1196 super::cmd_ledger(&rest);
1197 return;
1198 }
1199 "control" | "context-control" => {
1200 super::cmd_control(&rest);
1201 return;
1202 }
1203 "plan" | "context-plan" => {
1204 super::cmd_plan(&rest);
1205 return;
1206 }
1207 "compile" | "context-compile" => {
1208 super::cmd_compile(&rest);
1209 return;
1210 }
1211 "knowledge" => {
1212 super::cmd_knowledge(&rest);
1213 return;
1214 }
1215 "overview" => {
1216 super::cmd_overview(&rest);
1217 return;
1218 }
1219 "compress" => {
1220 super::cmd_compress(&rest);
1221 return;
1222 }
1223 "wrapped" => {
1224 super::cmd_wrapped(&rest);
1225 return;
1226 }
1227 "sessions" => {
1228 super::cmd_sessions(&rest);
1229 return;
1230 }
1231 "benchmark" => {
1232 super::cmd_benchmark(&rest);
1233 return;
1234 }
1235 "profile" => {
1236 super::cmd_profile(&rest);
1237 return;
1238 }
1239 "config" => {
1240 super::cmd_config(&rest);
1241 return;
1242 }
1243 "stats" => {
1244 super::cmd_stats(&rest);
1245 return;
1246 }
1247 "cache" => {
1248 super::cmd_cache(&rest);
1249 return;
1250 }
1251 "theme" => {
1252 super::cmd_theme(&rest);
1253 return;
1254 }
1255 "tee" => {
1256 super::cmd_tee(&rest);
1257 return;
1258 }
1259 "terse" | "compression" => {
1260 super::cmd_compression(&rest);
1261 return;
1262 }
1263 "slow-log" => {
1264 super::cmd_slow_log(&rest);
1265 return;
1266 }
1267 "update" | "--self-update" => {
1268 core::updater::run(&rest);
1269 return;
1270 }
1271 "restart" => {
1272 cmd_restart();
1273 return;
1274 }
1275 "stop" => {
1276 cmd_stop();
1277 return;
1278 }
1279 "dev-install" => {
1280 cmd_dev_install();
1281 return;
1282 }
1283 "doctor" => {
1284 let code = doctor::run_cli(&rest);
1285 if code != 0 {
1286 std::process::exit(code);
1287 }
1288 return;
1289 }
1290 "harden" => {
1291 super::harden::run(&rest);
1292 return;
1293 }
1294 "export-rules" => {
1295 super::export_rules::run(&rest);
1296 return;
1297 }
1298 "gotchas" | "bugs" => {
1299 super::cloud::cmd_gotchas(&rest);
1300 return;
1301 }
1302 "learn" => {
1303 super::cmd_learn(&rest);
1304 return;
1305 }
1306 "buddy" | "pet" => {
1307 super::cloud::cmd_buddy(&rest);
1308 return;
1309 }
1310 "hook" => {
1311 hook_handlers::mark_hook_environment();
1312 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
1313 let action = rest.first().map_or("help", std::string::String::as_str);
1314 match action {
1315 "rewrite" => hook_handlers::handle_rewrite(),
1316 "redirect" => hook_handlers::handle_redirect(),
1317 "observe" => hook_handlers::handle_observe(),
1318 "copilot" => hook_handlers::handle_copilot(),
1319 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
1320 "codex-session-start" => hook_handlers::handle_codex_session_start(),
1321 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
1322 _ => {
1323 eprintln!("Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>");
1324 eprintln!(" Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
1325 std::process::exit(1);
1326 }
1327 }
1328 return;
1329 }
1330 "report-issue" | "report" => {
1331 report::run(&rest);
1332 return;
1333 }
1334 "uninstall" => {
1335 let dry_run = rest.iter().any(|a| a == "--dry-run");
1336 uninstall::run(dry_run);
1337 return;
1338 }
1339 "bypass" => {
1340 if rest.is_empty() {
1341 eprintln!("Usage: lean-ctx bypass \"command\"");
1342 eprintln!("Runs the command with zero compression (raw passthrough).");
1343 std::process::exit(1);
1344 }
1345 let command = if rest.len() == 1 {
1346 rest[0].clone()
1347 } else {
1348 shell::join_command(&args[2..])
1349 };
1350 std::env::set_var("LEAN_CTX_RAW", "1");
1351 let code = shell::exec(&command);
1352 std::process::exit(code);
1353 }
1354 "safety-levels" | "safety" => {
1355 println!("{}", core::compression_safety::format_safety_table());
1356 return;
1357 }
1358 "cheat" | "cheatsheet" | "cheat-sheet" => {
1359 super::cmd_cheatsheet();
1360 return;
1361 }
1362 "login" => {
1363 super::cloud::cmd_login(&rest);
1364 return;
1365 }
1366 "register" => {
1367 super::cloud::cmd_register(&rest);
1368 return;
1369 }
1370 "forgot-password" => {
1371 super::cloud::cmd_forgot_password(&rest);
1372 return;
1373 }
1374 "sync" => {
1375 super::cloud::cmd_sync();
1376 return;
1377 }
1378 "contribute" => {
1379 super::cloud::cmd_contribute();
1380 return;
1381 }
1382 "cloud" => {
1383 super::cloud::cmd_cloud(&rest);
1384 return;
1385 }
1386 "upgrade" => {
1387 super::cloud::cmd_upgrade();
1388 return;
1389 }
1390 "--version" | "-V" => {
1391 println!("{}", core::integrity::origin_line());
1392 return;
1393 }
1394 "--help" | "-h" => {
1395 print_help();
1396 return;
1397 }
1398 "mcp" => {}
1399 _ => {
1400 tracing::error!("lean-ctx: unknown command '{}'", args[1]);
1401 print_help();
1402 std::process::exit(1);
1403 }
1404 }
1405 }
1406
1407 if let Err(e) = run_mcp_server() {
1408 tracing::error!("lean-ctx: {e}");
1409 std::process::exit(1);
1410 }
1411}
1412
1413fn passthrough(command: &str) -> ! {
1414 let (shell, flag) = shell::shell_and_flag();
1415 let status = std::process::Command::new(&shell)
1416 .arg(&flag)
1417 .arg(command)
1418 .env("LEAN_CTX_ACTIVE", "1")
1419 .status()
1420 .map_or(127, |s| s.code().unwrap_or(1));
1421 std::process::exit(status);
1422}
1423
1424fn run_async<F: std::future::Future>(future: F) -> F::Output {
1425 tokio::runtime::Runtime::new()
1426 .expect("failed to create async runtime")
1427 .block_on(future)
1428}
1429
1430fn run_mcp_server() -> Result<()> {
1431 use rmcp::ServiceExt;
1432
1433 std::env::set_var("LEAN_CTX_MCP_SERVER", "1");
1434
1435 crate::core::startup_guard::crash_loop_backoff("mcp-server");
1436
1437 let startup_lock = crate::core::startup_guard::try_acquire_lock(
1443 "mcp-startup",
1444 std::time::Duration::from_secs(3),
1445 std::time::Duration::from_secs(30),
1446 );
1447
1448 let parallelism = std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get);
1449 let worker_threads = resolve_worker_threads(parallelism);
1450 let max_blocking_threads = (worker_threads * 4).clamp(8, 32);
1451
1452 let rt = tokio::runtime::Builder::new_multi_thread()
1453 .worker_threads(worker_threads)
1454 .max_blocking_threads(max_blocking_threads)
1455 .enable_all()
1456 .build()?;
1457
1458 let server = tools::create_server();
1459 drop(startup_lock);
1460
1461 spawn_proxy_if_needed();
1463
1464 rt.block_on(async {
1465 core::logging::init_mcp_logging();
1466 core::protocol::set_mcp_context(true);
1467
1468 tracing::info!(
1469 "lean-ctx v{} MCP server starting",
1470 env!("CARGO_PKG_VERSION")
1471 );
1472
1473 let transport =
1474 mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
1475 let server_handle = server.clone();
1476 let service = match server.serve(transport).await {
1477 Ok(s) => s,
1478 Err(e) => {
1479 let msg = e.to_string();
1480 if msg.contains("expect initialized")
1481 || msg.contains("context canceled")
1482 || msg.contains("broken pipe")
1483 {
1484 tracing::debug!("Client disconnected before init: {msg}");
1485 return Ok(());
1486 }
1487 return Err(e.into());
1488 }
1489 };
1490 match service.waiting().await {
1491 Ok(reason) => {
1492 tracing::info!("MCP server stopped: {reason:?}");
1493 }
1494 Err(e) => {
1495 let msg = e.to_string();
1496 if msg.contains("broken pipe")
1497 || msg.contains("connection reset")
1498 || msg.contains("context canceled")
1499 {
1500 tracing::info!("MCP server: transport closed ({msg})");
1501 } else {
1502 tracing::error!("MCP server error: {msg}");
1503 }
1504 }
1505 }
1506
1507 server_handle.shutdown().await;
1508
1509 core::stats::flush();
1510 core::heatmap::flush();
1511 core::mode_predictor::ModePredictor::flush();
1512 core::feedback::FeedbackStore::flush();
1513
1514 Ok(())
1515 })
1516}
1517
1518fn print_help() {
1519 println!(
1520 "lean-ctx {version} — Context Runtime for AI Agents
1521
152260+ compression patterns | 51 MCP tools | 10 read modes | Context Continuity Protocol
1523
1524USAGE:
1525 lean-ctx Start MCP server (stdio)
1526 lean-ctx serve Start MCP server (Streamable HTTP)
1527 lean-ctx serve --daemon Start as background daemon (Unix Domain Socket)
1528 lean-ctx serve --stop Stop running daemon
1529 lean-ctx serve --status Show daemon status
1530 lean-ctx -t \"command\" Track command (full output + stats, no compression)
1531 lean-ctx -c \"command\" Execute with compressed output (used by AI hooks)
1532 lean-ctx -c --raw \"command\" Execute without compression (full output)
1533 lean-ctx exec \"command\" Same as -c
1534 lean-ctx shell Interactive shell with compression
1535
1536COMMANDS:
1537 gain Visual dashboard (colors, bars, sparklines, USD)
1538 gain --live Live mode: auto-refreshes every 1s in-place
1539 gain --graph 30-day savings chart
1540 gain --daily Bordered day-by-day table with USD
1541 gain --json Raw JSON export of all stats
1542 token-report [--json] Token + memory report (project + session + CEP)
1543 pack --pr PR Context Pack (changed files, impact, tests, artifacts)
1544 index <status|build|build-full|watch> Codebase index utilities
1545 cep CEP impact report (score trends, cache, modes)
1546 watch Live TUI dashboard (real-time event stream)
1547 dashboard [--port=N] [--host=H] Open web dashboard (default: http://localhost:3333)
1548 serve [--host H] [--port N] MCP over HTTP (Streamable HTTP, local-first)
1549 proxy start [--port=4444] API proxy: compress tool_results before LLM API
1550 proxy status Show proxy statistics
1551 cache [list|clear|stats] Show/manage file read cache
1552 wrapped [--week|--month|--all] Deprecated alias for gain --wrapped
1553 sessions [list|show|cleanup] Manage CCP sessions (~/.lean-ctx/sessions/)
1554 benchmark run [path] [--json] Run real benchmark on project files
1555 benchmark report [path] Generate shareable Markdown report
1556 cheatsheet Command cheat sheet & workflow quick reference
1557 setup One-command setup: shell + editor + verify
1558 install --repair [--json] Premium repair: merge-based setup refresh (no deletes)
1559 bootstrap Non-interactive setup + fix (zero-config)
1560 status [--json] Show setup + MCP + rules status
1561 init [--global] Install shell aliases (zsh/bash/fish/PowerShell)
1562 init --agent <name> Configure MCP for specific editor/agent
1563 read <file> [-m mode] Read file with compression
1564 diff <file1> <file2> Compressed file diff
1565 grep <pattern> [path] Search with compressed output
1566 find <pattern> [path] Find files with compressed output
1567 ls [path] Directory listing with compression
1568 deps [path] Show project dependencies
1569 discover Find uncompressed commands in shell history
1570 ghost [--json] Ghost Token report: find hidden token waste
1571 filter [list|validate|init] Manage custom compression filters (~/.lean-ctx/filters/)
1572 session Show adoption statistics
1573 session task <desc> Set current task
1574 session finding <summary> Record a finding
1575 session save Save current session
1576 session load [id] Load session (latest if no ID)
1577 knowledge remember <value> --category <c> --key <k> Store a fact
1578 knowledge recall [query] [--category <c>] Retrieve facts
1579 knowledge search <query> Cross-project knowledge search
1580 knowledge export [--format json|jsonl|simple] [--output <path>] Export knowledge
1581 knowledge import <path> [--merge replace|append|skip-existing] Import knowledge
1582 knowledge remove --category <c> --key <k> Remove a fact
1583 knowledge status Knowledge base summary
1584 overview [task] Project overview (task-contextualized if given)
1585 compress [--signatures] Context compression checkpoint
1586 config Show/edit configuration (~/.lean-ctx/config.toml)
1587 profile [list|show|diff|create|set] Manage context profiles
1588 theme [list|set|export|import] Customize terminal colors and themes
1589 tee [list|clear|show <file>|last] Manage output tee files (~/.lean-ctx/tee/)
1590 terse [off|lite|full|ultra] Set agent output verbosity (saves 25-65% output tokens)
1591 slow-log [list|clear] Show/clear slow command log (~/.lean-ctx/slow-commands.log)
1592 update [--check] Self-update lean-ctx binary from GitHub Releases
1593 stop Stop ALL lean-ctx processes (daemon, proxy, orphans)
1594 restart Restart daemon (applies config.toml changes)
1595 dev-install Build release + atomic install + restart (for development)
1596 gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
1597 buddy [show|stats|ascii|json] Token Guardian: your data-driven coding companion
1598 doctor integrations [--json] Integration health checks (Cursor/Claude Code)
1599 doctor [--fix] [--json] Run diagnostics (and optionally repair)
1600 smells [scan|summary|rules|file] [--rule=<r>] [--path=<p>] [--json]
1601 Code smell detection (Property Graph, 8 rules)
1602 control <action> [--target=<t>] Context field manipulation (exclude/pin/priority)
1603 plan <task> [--budget=N] Context planning (optimal Phi-scored context plan)
1604 compile [--mode=<m>] [--budget=N] Context compilation (knapsack + Boltzmann)
1605 uninstall Remove shell hook, MCP configs, and data directory
1606
1607SHELL HOOK PATTERNS (95+):
1608 git status, log, diff, add, commit, push, pull, fetch, clone,
1609 branch, checkout, switch, merge, stash, tag, reset, remote
1610 docker build, ps, images, logs, compose, exec, network
1611 npm/pnpm install, test, run, list, outdated, audit
1612 cargo build, test, check, clippy
1613 gh pr list/view/create, issue list/view, run list/view
1614 kubectl get pods/services/deployments, logs, describe, apply
1615 python pip install/list/outdated, ruff check/format, poetry, uv
1616 linters eslint, biome, prettier, golangci-lint
1617 builds tsc, next build, vite build
1618 ruby rubocop, bundle install/update, rake test, rails test
1619 tests jest, vitest, pytest, go test, playwright, rspec, minitest
1620 iac terraform, make, maven, gradle, dotnet, flutter, dart
1621 utils curl, grep/rg, find, ls, wget, env
1622 data JSON schema extraction, log deduplication
1623
1624READ MODES:
1625 auto Auto-select optimal mode (default)
1626 full Full content (cached re-reads = 13 tokens)
1627 map Dependency graph + API signatures
1628 signatures tree-sitter AST extraction (18 languages)
1629 task Task-relevant filtering (requires ctx_session task)
1630 reference One-line reference stub (cheap cache key)
1631 aggressive Syntax-stripped content
1632 entropy Shannon entropy filtered
1633 diff Changed lines only
1634 lines:N-M Specific line ranges (e.g. lines:10-50,80)
1635
1636ENVIRONMENT:
1637 LEAN_CTX_DISABLED=1 Bypass ALL compression + prevent shell hook from loading
1638 LEAN_CTX_ENABLED=0 Prevent shell hook auto-start (lean-ctx-on still works)
1639 LEAN_CTX_RAW=1 Same as --raw for current command
1640 LEAN_CTX_AUTONOMY=false Disable autonomous features
1641 LEAN_CTX_COMPRESS=1 Force compression (even for excluded commands)
1642
1643OPTIONS:
1644 --version, -V Show version
1645 --help, -h Show this help
1646
1647EXAMPLES:
1648 lean-ctx -c \"git status\" Compressed git output
1649 lean-ctx -c \"kubectl get pods\" Compressed k8s output
1650 lean-ctx -c \"gh pr list\" Compressed GitHub CLI output
1651 lean-ctx gain Visual terminal dashboard
1652 lean-ctx gain --live Live auto-updating terminal dashboard
1653 lean-ctx gain --graph 30-day savings chart
1654 lean-ctx gain --daily Day-by-day breakdown with USD
1655 lean-ctx token-report --json Machine-readable token + memory report
1656 lean-ctx dashboard Open web dashboard at localhost:3333
1657 lean-ctx dashboard --host=0.0.0.0 Bind to all interfaces (remote access)
1658 lean-ctx gain --wrapped Wrapped report card (recommended)
1659 lean-ctx gain --wrapped --period=month Monthly Wrapped report card
1660 lean-ctx sessions list List all CCP sessions
1661 lean-ctx sessions show Show latest session state
1662 lean-ctx discover Find missed savings in shell history
1663 lean-ctx setup One-command setup (shell + editors + verify)
1664 lean-ctx install --repair Premium repair path (non-interactive, merge-based)
1665 lean-ctx bootstrap Non-interactive setup + fix (zero-config)
1666 lean-ctx bootstrap --json Machine-readable bootstrap report
1667 lean-ctx init --global Install shell aliases (includes lean-ctx-on/off/mode/status)
1668 lean-ctx-on Enable shell aliases in track mode (full output + stats)
1669 lean-ctx-off Disable all shell aliases
1670 lean-ctx-mode track Track mode: full output, stats recorded (default)
1671 lean-ctx-mode compress Compress mode: all output compressed (power users)
1672 lean-ctx-mode off Same as lean-ctx-off
1673 lean-ctx-status Show whether compression is active
1674 lean-ctx init --agent pi Install Pi Coding Agent extension
1675 lean-ctx doctor Check PATH, config, MCP, and dashboard port
1676 lean-ctx doctor integrations Premium integration checks (Cursor/Claude Code)
1677 lean-ctx doctor --fix --json Repair + machine-readable report
1678 lean-ctx status --json Machine-readable current status
1679 lean-ctx session task \"implement auth\"
1680 lean-ctx session finding \"auth.rs:42 — missing validation\"
1681 lean-ctx knowledge remember \"Uses JWT\" --category auth --key token-type
1682 lean-ctx knowledge recall \"authentication\"
1683 lean-ctx knowledge search \"database migration\"
1684 lean-ctx overview \"refactor auth module\"
1685 lean-ctx compress --signatures
1686 lean-ctx read src/main.rs -m map
1687 lean-ctx grep \"pub fn\" src/
1688 lean-ctx deps .
1689
1690CLOUD:
1691 cloud status Show cloud connection status
1692 login <email> Log into existing LeanCTX Cloud account
1693 register <email> Create a new LeanCTX Cloud account
1694 forgot-password <email> Send password reset email
1695 sync Upload local stats to cloud dashboard
1696 contribute Share anonymized compression data
1697
1698TROUBLESHOOTING:
1699 Commands broken? lean-ctx-off (fixes current session)
1700 Permanent fix? lean-ctx uninstall (removes all hooks)
1701 Manual fix? Edit ~/.zshrc, remove the \"lean-ctx shell hook\" block
1702 Binary missing? Aliases auto-fallback to original commands (safe)
1703 Preview init? lean-ctx init --global --dry-run
1704
1705WEBSITE: https://leanctx.com
1706GITHUB: https://github.com/yvgude/lean-ctx
1707",
1708 version = env!("CARGO_PKG_VERSION"),
1709 );
1710}
1711
1712fn cmd_stop() {
1713 use crate::daemon;
1714 use crate::ipc;
1715
1716 eprintln!("Stopping all lean-ctx processes…");
1717
1718 crate::proxy_autostart::stop();
1720 eprintln!(" Unloaded autostart (LaunchAgent/systemd).");
1721
1722 if let Err(e) = daemon::stop_daemon() {
1724 eprintln!(" Warning: daemon stop: {e}");
1725 }
1726
1727 let killed = ipc::process::kill_all_by_name("lean-ctx");
1729 if killed > 0 {
1730 eprintln!(" Sent SIGTERM to {killed} process(es).");
1731 }
1732
1733 std::thread::sleep(std::time::Duration::from_millis(500));
1734
1735 let remaining = ipc::process::find_killable_pids("lean-ctx");
1737 if !remaining.is_empty() {
1738 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
1739 for &pid in &remaining {
1740 let _ = ipc::process::force_kill(pid);
1741 }
1742 std::thread::sleep(std::time::Duration::from_millis(300));
1743 }
1744
1745 daemon::cleanup_daemon_files();
1746
1747 let final_check = ipc::process::find_killable_pids("lean-ctx");
1748 if final_check.is_empty() {
1749 eprintln!(" ✓ All lean-ctx processes stopped.");
1750 } else {
1751 eprintln!(
1752 " ✗ {} process(es) could not be killed: {:?}",
1753 final_check.len(),
1754 final_check
1755 );
1756 eprintln!(
1757 " Try: sudo kill -9 {}",
1758 final_check
1759 .iter()
1760 .map(std::string::ToString::to_string)
1761 .collect::<Vec<_>>()
1762 .join(" ")
1763 );
1764 std::process::exit(1);
1765 }
1766}
1767
1768fn cmd_restart() {
1769 use crate::daemon;
1770 use crate::ipc;
1771
1772 eprintln!("Restarting lean-ctx…");
1773
1774 crate::proxy_autostart::stop();
1776
1777 if let Err(e) = daemon::stop_daemon() {
1778 eprintln!(" Warning: daemon stop: {e}");
1779 }
1780
1781 let orphans = ipc::process::kill_all_by_name("lean-ctx");
1782 if orphans > 0 {
1783 eprintln!(" Terminated {orphans} orphan process(es).");
1784 }
1785
1786 std::thread::sleep(std::time::Duration::from_millis(500));
1787
1788 let remaining = ipc::process::find_pids_by_name("lean-ctx");
1789 if !remaining.is_empty() {
1790 eprintln!(
1791 " Force-killing {} stubborn process(es): {:?}",
1792 remaining.len(),
1793 remaining
1794 );
1795 for &pid in &remaining {
1796 let _ = ipc::process::force_kill(pid);
1797 }
1798 std::thread::sleep(std::time::Duration::from_millis(300));
1799 }
1800
1801 daemon::cleanup_daemon_files();
1802
1803 crate::proxy_autostart::start();
1805
1806 match daemon::start_daemon(&[]) {
1807 Ok(()) => eprintln!(" ✓ Daemon restarted. Config changes are now active."),
1808 Err(e) => {
1809 eprintln!(" ✗ Daemon start failed: {e}");
1810 std::process::exit(1);
1811 }
1812 }
1813}
1814
1815fn cmd_dev_install() {
1816 use crate::ipc;
1817
1818 let cargo_root = find_cargo_project_root();
1819 let Some(cargo_root) = cargo_root else {
1820 eprintln!("Error: No Cargo.toml found. Run from the lean-ctx project directory.");
1821 std::process::exit(1);
1822 };
1823
1824 eprintln!("Building release binary…");
1825 let build = std::process::Command::new("cargo")
1826 .args(["build", "--release"])
1827 .current_dir(&cargo_root)
1828 .status();
1829
1830 match build {
1831 Ok(s) if s.success() => {}
1832 Ok(s) => {
1833 eprintln!(" Build failed with exit code {}", s.code().unwrap_or(-1));
1834 std::process::exit(1);
1835 }
1836 Err(e) => {
1837 eprintln!(" Build failed: {e}");
1838 std::process::exit(1);
1839 }
1840 }
1841
1842 let built_binary = cargo_root.join("target/release/lean-ctx");
1843 if !built_binary.exists() {
1844 eprintln!(
1845 " Error: Built binary not found at {}",
1846 built_binary.display()
1847 );
1848 std::process::exit(1);
1849 }
1850
1851 let install_path = resolve_install_path();
1852 eprintln!("Installing to {}…", install_path.display());
1853
1854 eprintln!(" Stopping all lean-ctx processes…");
1855 crate::proxy_autostart::stop();
1856 let _ = crate::daemon::stop_daemon();
1857 ipc::process::kill_all_by_name("lean-ctx");
1858 std::thread::sleep(std::time::Duration::from_millis(500));
1859
1860 let remaining = ipc::process::find_pids_by_name("lean-ctx");
1861 if !remaining.is_empty() {
1862 eprintln!(" Force-killing {} stubborn process(es)…", remaining.len());
1863 for &pid in &remaining {
1864 let _ = ipc::process::force_kill(pid);
1865 }
1866 std::thread::sleep(std::time::Duration::from_millis(500));
1867 }
1868
1869 let old_path = install_path.with_extension("old");
1870 if install_path.exists() {
1871 if let Err(e) = std::fs::rename(&install_path, &old_path) {
1872 eprintln!(" Warning: rename existing binary: {e}");
1873 }
1874 }
1875
1876 match std::fs::copy(&built_binary, &install_path) {
1877 Ok(_) => {
1878 let _ = std::fs::remove_file(&old_path);
1879 #[cfg(unix)]
1880 {
1881 use std::os::unix::fs::PermissionsExt;
1882 let _ =
1883 std::fs::set_permissions(&install_path, std::fs::Permissions::from_mode(0o755));
1884 }
1885 eprintln!(" ✓ Binary installed.");
1886 }
1887 Err(e) => {
1888 eprintln!(" Error: copy failed: {e}");
1889 if old_path.exists() {
1890 let _ = std::fs::rename(&old_path, &install_path);
1891 eprintln!(" Rolled back to previous binary.");
1892 }
1893 std::process::exit(1);
1894 }
1895 }
1896
1897 let version = std::process::Command::new(&install_path)
1898 .arg("--version")
1899 .output()
1900 .map_or_else(
1901 |_| "unknown".to_string(),
1902 |o| String::from_utf8_lossy(&o.stdout).trim().to_string(),
1903 );
1904
1905 eprintln!(" ✓ dev-install complete: {version}");
1906
1907 eprintln!(" Re-enabling autostart…");
1908 crate::proxy_autostart::start();
1909
1910 eprintln!(" Starting daemon…");
1911 match crate::daemon::start_daemon(&[]) {
1912 Ok(()) => {}
1913 Err(e) => eprintln!(" Warning: daemon start: {e} (will be started by editor)"),
1914 }
1915}
1916
1917fn find_cargo_project_root() -> Option<std::path::PathBuf> {
1918 let mut dir = std::env::current_dir().ok()?;
1919 loop {
1920 if dir.join("Cargo.toml").exists() {
1921 return Some(dir);
1922 }
1923 if !dir.pop() {
1924 return None;
1925 }
1926 }
1927}
1928
1929fn resolve_install_path() -> std::path::PathBuf {
1930 if let Ok(exe) = std::env::current_exe() {
1931 if let Ok(canonical) = exe.canonicalize() {
1932 let is_in_cargo_target = canonical.components().any(|c| c.as_os_str() == "target");
1933 if !is_in_cargo_target && canonical.exists() {
1934 return canonical;
1935 }
1936 }
1937 }
1938
1939 if let Ok(home) = std::env::var("HOME") {
1940 let local_bin = std::path::PathBuf::from(&home).join(".local/bin/lean-ctx");
1941 if local_bin.parent().is_some_and(std::path::Path::exists) {
1942 return local_bin;
1943 }
1944 }
1945
1946 std::path::PathBuf::from("/usr/local/bin/lean-ctx")
1947}
1948
1949fn spawn_proxy_if_needed() {
1950 use std::net::TcpStream;
1951 use std::time::Duration;
1952
1953 let port = crate::proxy_setup::default_port();
1954 let already_running = TcpStream::connect_timeout(
1955 &format!("127.0.0.1:{port}").parse().unwrap(),
1956 Duration::from_millis(200),
1957 )
1958 .is_ok();
1959
1960 if already_running {
1961 tracing::debug!("proxy already running on port {port}");
1962 return;
1963 }
1964
1965 let binary = std::env::current_exe().map_or_else(
1966 |_| "lean-ctx".to_string(),
1967 |p| p.to_string_lossy().to_string(),
1968 );
1969
1970 match std::process::Command::new(&binary)
1971 .args(["proxy", "start", &format!("--port={port}")])
1972 .stdin(std::process::Stdio::null())
1973 .stdout(std::process::Stdio::null())
1974 .stderr(std::process::Stdio::null())
1975 .spawn()
1976 {
1977 Ok(_) => tracing::info!("auto-started proxy on port {port}"),
1978 Err(e) => tracing::debug!("could not auto-start proxy: {e}"),
1979 }
1980}
1981
1982fn resolve_worker_threads(parallelism: usize) -> usize {
1983 std::env::var("LEAN_CTX_WORKER_THREADS")
1984 .ok()
1985 .and_then(|v| v.parse::<usize>().ok())
1986 .unwrap_or_else(|| parallelism.clamp(1, 4))
1987}
1988
1989#[cfg(test)]
1990mod tests {
1991 use super::*;
1992 use serial_test::serial;
1993
1994 #[test]
1995 #[serial]
1996 fn worker_threads_default_clamps_low() {
1997 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
1998 assert_eq!(resolve_worker_threads(1), 1);
1999 }
2000
2001 #[test]
2002 #[serial]
2003 fn worker_threads_default_clamps_high() {
2004 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2005 assert_eq!(resolve_worker_threads(32), 4);
2006 }
2007
2008 #[test]
2009 #[serial]
2010 fn worker_threads_default_passthrough() {
2011 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2012 assert_eq!(resolve_worker_threads(3), 3);
2013 }
2014
2015 #[test]
2016 #[serial]
2017 fn worker_threads_env_override() {
2018 std::env::set_var("LEAN_CTX_WORKER_THREADS", "12");
2019 assert_eq!(resolve_worker_threads(2), 12);
2020 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2021 }
2022
2023 #[test]
2024 #[serial]
2025 fn worker_threads_env_invalid_falls_back() {
2026 std::env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
2027 assert_eq!(resolve_worker_threads(3), 3);
2028 std::env::remove_var("LEAN_CTX_WORKER_THREADS");
2029 }
2030}