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