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