Skip to main content

lean_ctx/cli/
dispatch.rs

1use crate::{
2    core, doctor, hook_handlers, mcp_stdio, report, setup, shell,
3    status, token_report, tools, uninstall,
4};
5use anyhow::Result;
6
7fn fire_shell_telemetry(tool_name: String) {
8    core::telemetry_queue::fire_sync(crate::models::TelemetryIngestRequest {
9        tool_name,
10        tokens_original: 0,
11        tokens_saved: 0,
12        duration_ms: 0,
13        mode: Some("shell".to_string()),
14        repository_fingerprint: None,
15        checkout_binding: None,
16        project_slug: None,
17    });
18}
19
20
21pub fn run() {
22    let args: Vec<String> = std::env::args().collect();
23
24    if args.len() > 1 {
25        let rest = args[2..].to_vec();
26        let command = args[1].as_str();
27
28        if matches!(command, "heatmap" | "stats") {
29            super::exit_hosted_analytics_only(command);
30        }
31
32        match command {
33            "-c" | "exec" => {
34                let raw = rest.first().map(|a| a == "--raw").unwrap_or(false);
35                let cmd_args = if raw { &args[3..] } else { &args[2..] };
36                let command = if cmd_args.len() == 1 {
37                    cmd_args[0].clone()
38                } else {
39                    shell::join_command(cmd_args)
40                };
41                if std::env::var("NEBU_CTX_ACTIVE").is_ok()
42                    || std::env::var("NEBU_CTX_DISABLED").is_ok()
43                {
44                    fire_shell_telemetry(core::stats::normalize_command(&command));
45                    passthrough(&command);
46                }
47                if raw {
48                    std::env::set_var("NEBU_CTX_RAW", "1");
49                } else {
50                    std::env::set_var("NEBU_CTX_COMPRESS", "1");
51                }
52                let code = shell::exec(&command);
53                core::stats::flush();
54                fire_shell_telemetry(core::stats::normalize_command(&command));
55                std::process::exit(code);
56            }
57            "-t" | "--track" => {
58                let cmd_args = &args[2..];
59                let tracked_name = cmd_args
60                    .first()
61                    .map(|s| core::stats::normalize_command(s))
62                    .unwrap_or_else(|| "shell".to_string());
63                let code = if cmd_args.len() > 1 {
64                    shell::exec_argv(cmd_args)
65                } else {
66                    let command = cmd_args[0].clone();
67                    if std::env::var("NEBU_CTX_ACTIVE").is_ok()
68                        || std::env::var("NEBU_CTX_DISABLED").is_ok()
69                    {
70                        fire_shell_telemetry(tracked_name.clone());
71                        passthrough(&command);
72                    }
73                    shell::exec(&command)
74                };
75                core::stats::flush();
76                fire_shell_telemetry(tracked_name);
77                std::process::exit(code);
78            }
79            "shell" | "--shell" => {
80                shell::interactive();
81                return;
82            }
83            "gain" => {
84                if rest.iter().any(|a| a == "--reset") {
85                    core::stats::reset_all();
86                    println!("Stats reset. All token savings data cleared.");
87                    return;
88                }
89                if super::has_flag(&rest, &["--live", "--watch"]) {
90                    core::stats::gain_live();
91                    return;
92                }
93                let model = super::option_value(&rest, &["--model"]);
94                let period = super::option_value(&rest, &["--period"])
95                    .unwrap_or_else(|| "all".to_string());
96                let limit = super::option_value_parsed::<usize>(&rest, &["--limit"]).unwrap_or(10);
97
98                if rest.iter().any(|a| a == "--graph") {
99                    println!("{}", core::stats::format_gain_graph());
100                } else if rest.iter().any(|a| a == "--daily") {
101                    println!("{}", core::stats::format_gain_daily());
102                } else if rest.iter().any(|a| a == "--json") {
103                    println!(
104                        "{}",
105                        tools::ctx_gain::handle(
106                            "json",
107                            Some(&period),
108                            model.as_deref(),
109                            Some(limit)
110                        )
111                    );
112                } else if rest.iter().any(|a| a == "--score") {
113                    println!(
114                        "{}",
115                        tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
116                    );
117                } else if rest.iter().any(|a| a == "--cost") {
118                    println!(
119                        "{}",
120                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
121                    );
122                } else if rest.iter().any(|a| a == "--tasks") {
123                    println!(
124                        "{}",
125                        tools::ctx_gain::handle("tasks", None, None, Some(limit))
126                    );
127                } else if rest.iter().any(|a| a == "--agents") {
128                    println!(
129                        "{}",
130                        tools::ctx_gain::handle("agents", None, None, Some(limit))
131                    );
132                } else if rest.iter().any(|a| a == "--heatmap") {
133                    println!(
134                        "{}",
135                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
136                    );
137                } else if rest.iter().any(|a| a == "--wrapped") {
138                    println!(
139                        "{}",
140                        tools::ctx_gain::handle(
141                            "wrapped",
142                            Some(&period),
143                            model.as_deref(),
144                            Some(limit)
145                        )
146                    );
147                } else if rest.iter().any(|a| a == "--pipeline") {
148                    let stats_path = dirs::home_dir()
149                        .unwrap_or_default()
150                        .join(".nebu-ctx")
151                        .join("pipeline_stats.json");
152                    if let Ok(data) = std::fs::read_to_string(&stats_path) {
153                        if let Ok(stats) =
154                            serde_json::from_str::<core::pipeline::PipelineStats>(&data)
155                        {
156                            println!("{}", stats.format_summary());
157                        } else {
158                            println!("No pipeline stats available yet (corrupt data).");
159                        }
160                    } else {
161                        println!(
162                            "No pipeline stats available yet. Use MCP tools to generate data."
163                        );
164                    }
165                } else if rest.iter().any(|a| a == "--deep") {
166                    println!(
167                        "{}\n{}\n{}\n{}\n{}",
168                        tools::ctx_gain::handle("report", None, model.as_deref(), Some(limit)),
169                        tools::ctx_gain::handle("tasks", None, None, Some(limit)),
170                        tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit)),
171                        tools::ctx_gain::handle("agents", None, None, Some(limit)),
172                        tools::ctx_gain::handle("heatmap", None, None, Some(limit))
173                    );
174                } else {
175                    println!("{}", core::stats::format_gain());
176                }
177                return;
178            }
179            "token-report" | "report-tokens" => {
180                let code = token_report::run_cli(&rest);
181                if code != 0 {
182                    std::process::exit(code);
183                }
184                return;
185            }
186            "cep" => {
187                println!("{}", tools::ctx_gain::handle("score", None, None, Some(10)));
188                return;
189            }
190            "serve" => {
191                #[cfg(feature = "http-server")]
192                {
193                    let mut cfg = crate::mcp_http::HttpServerConfig::default();
194                    let mut i = 0;
195                    while i < rest.len() {
196                        match rest[i].as_str() {
197                            "--host" | "-H" => {
198                                i += 1;
199                                if i < rest.len() {
200                                    cfg.host = rest[i].clone();
201                                }
202                            }
203                            arg if arg.starts_with("--host=") => {
204                                cfg.host = arg["--host=".len()..].to_string();
205                            }
206                            "--port" | "-p" => {
207                                i += 1;
208                                if i < rest.len() {
209                                    if let Ok(p) = rest[i].parse::<u16>() {
210                                        cfg.port = p;
211                                    }
212                                }
213                            }
214                            arg if arg.starts_with("--port=") => {
215                                if let Ok(p) = arg["--port=".len()..].parse::<u16>() {
216                                    cfg.port = p;
217                                }
218                            }
219                            "--project-root" => {
220                                i += 1;
221                                if i < rest.len() {
222                                    cfg.project_root = std::path::PathBuf::from(&rest[i]);
223                                }
224                            }
225                            arg if arg.starts_with("--project-root=") => {
226                                cfg.project_root =
227                                    std::path::PathBuf::from(&arg["--project-root=".len()..]);
228                            }
229                            "--auth-token" => {
230                                i += 1;
231                                if i < rest.len() {
232                                    cfg.auth_token = Some(rest[i].clone());
233                                }
234                            }
235                            arg if arg.starts_with("--auth-token=") => {
236                                cfg.auth_token = Some(arg["--auth-token=".len()..].to_string());
237                            }
238                            "--stateful" => cfg.stateful_mode = true,
239                            "--stateless" => cfg.stateful_mode = false,
240                            "--json" => cfg.json_response = true,
241                            "--sse" => cfg.json_response = false,
242                            "--disable-host-check" => cfg.disable_host_check = true,
243                            "--allowed-host" => {
244                                i += 1;
245                                if i < rest.len() {
246                                    cfg.allowed_hosts.push(rest[i].clone());
247                                }
248                            }
249                            arg if arg.starts_with("--allowed-host=") => {
250                                cfg.allowed_hosts
251                                    .push(arg["--allowed-host=".len()..].to_string());
252                            }
253                            "--max-body-bytes" => {
254                                i += 1;
255                                if i < rest.len() {
256                                    if let Ok(n) = rest[i].parse::<usize>() {
257                                        cfg.max_body_bytes = n;
258                                    }
259                                }
260                            }
261                            arg if arg.starts_with("--max-body-bytes=") => {
262                                if let Ok(n) = arg["--max-body-bytes=".len()..].parse::<usize>() {
263                                    cfg.max_body_bytes = n;
264                                }
265                            }
266                            "--max-concurrency" => {
267                                i += 1;
268                                if i < rest.len() {
269                                    if let Ok(n) = rest[i].parse::<usize>() {
270                                        cfg.max_concurrency = n;
271                                    }
272                                }
273                            }
274                            arg if arg.starts_with("--max-concurrency=") => {
275                                if let Ok(n) = arg["--max-concurrency=".len()..].parse::<usize>() {
276                                    cfg.max_concurrency = n;
277                                }
278                            }
279                            "--max-rps" => {
280                                i += 1;
281                                if i < rest.len() {
282                                    if let Ok(n) = rest[i].parse::<u32>() {
283                                        cfg.max_rps = n;
284                                    }
285                                }
286                            }
287                            arg if arg.starts_with("--max-rps=") => {
288                                if let Ok(n) = arg["--max-rps=".len()..].parse::<u32>() {
289                                    cfg.max_rps = n;
290                                }
291                            }
292                            "--rate-burst" => {
293                                i += 1;
294                                if i < rest.len() {
295                                    if let Ok(n) = rest[i].parse::<u32>() {
296                                        cfg.rate_burst = n;
297                                    }
298                                }
299                            }
300                            arg if arg.starts_with("--rate-burst=") => {
301                                if let Ok(n) = arg["--rate-burst=".len()..].parse::<u32>() {
302                                    cfg.rate_burst = n;
303                                }
304                            }
305                            "--request-timeout-ms" => {
306                                i += 1;
307                                if i < rest.len() {
308                                    if let Ok(n) = rest[i].parse::<u64>() {
309                                        cfg.request_timeout_ms = n;
310                                    }
311                                }
312                            }
313                            arg if arg.starts_with("--request-timeout-ms=") => {
314                                if let Ok(n) = arg["--request-timeout-ms=".len()..].parse::<u64>() {
315                                    cfg.request_timeout_ms = n;
316                                }
317                            }
318                            "--help" | "-h" => {
319                                eprintln!(
320                                                                        "Usage: nebu-ctx serve [--host H] [--port N] [--project-root DIR]\\n\\
321                                     \\n\\
322                                     Options:\\n\\
323                                       --host, -H            Bind host (default: 127.0.0.1)\\n\\
324                                       --port, -p            Bind port (default: 8080)\\n\\
325                                       --project-root        Resolve relative paths against this root (default: cwd)\\n\\
326                                       --auth-token          Require Authorization: Bearer <token> (required for non-loopback hosts)\\n\\
327                                       --stateful/--stateless  Streamable HTTP session mode (default: stateless)\\n\\
328                                       --json/--sse          Response framing in stateless mode (default: json)\\n\\
329                                       --max-body-bytes      Max request body size in bytes (default: 2097152)\\n\\
330                                       --max-concurrency     Max concurrent requests (default: 32)\\n\\
331                                       --max-rps             Max requests/sec (global, default: 50)\\n\\
332                                       --rate-burst          Rate limiter burst (global, default: 100)\\n\\
333                                       --request-timeout-ms  REST tool-call timeout (default: 30000)\\n\\
334                                       --allowed-host        Add allowed Host header (repeatable)\\n\\
335                                       --disable-host-check  Disable Host header validation (unsafe)"
336                                );
337                                return;
338                            }
339                            _ => {}
340                        }
341                        i += 1;
342                    }
343
344                    if cfg.auth_token.is_none() {
345                        if let Ok(v) = std::env::var("NEBU_CTX_HTTP_TOKEN") {
346                            if !v.trim().is_empty() {
347                                cfg.auth_token = Some(v);
348                            }
349                        }
350                    }
351
352                    if let Err(e) = run_async(crate::mcp_http::serve(cfg)) {
353                        eprintln!("HTTP server error: {e}");
354                        std::process::exit(1);
355                    }
356                    return;
357                }
358                #[cfg(not(feature = "http-server"))]
359                {
360                    eprintln!("nebu-ctx serve is not available in this build");
361                    std::process::exit(1);
362                }
363            }
364            "proxy" => {
365                #[cfg(feature = "http-server")]
366                {
367                    let sub = rest.first().map(|s| s.as_str()).unwrap_or("help");
368                    match sub {
369                        "start" => {
370                            let port = super::option_value_parsed::<u16>(&rest, &["--port", "-p"])
371                                .unwrap_or(4444);
372                            let autostart = rest.iter().any(|a| a == "--autostart");
373                            if autostart {
374                                crate::proxy_autostart::install(port, false);
375                                return;
376                            }
377                            if let Err(e) = run_async(crate::llm_proxy::start_proxy(port)) {
378                                eprintln!("Proxy error: {e}");
379                                std::process::exit(1);
380                            }
381                        }
382                        "stop" => {
383                            let port = super::option_value_parsed::<u16>(&rest, &["--port", "-p"])
384                                .unwrap_or(4444);
385                            match ureq::get(&format!(
386                                "http://127.0.0.1:{port}/health"
387                            ))
388                            .call()
389                            {
390                                Ok(_) => {
391                                    println!("Proxy is running. Use Ctrl+C or kill the process.");
392                                }
393                                Err(_) => {
394                                    println!("No proxy running on that port.");
395                                }
396                            }
397                        }
398                        "status" => {
399                            let port = super::option_value_parsed::<u16>(&rest, &["--port", "-p"])
400                                .unwrap_or(4444);
401                            match ureq::get(&format!("http://127.0.0.1:{port}/status")).call() {
402                                Ok(resp) => {
403                                    let body =
404                                        resp.into_body().read_to_string().unwrap_or_default();
405                                    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body)
406                                    {
407                                        println!("nebu-ctx proxy status:");
408                                        println!("  Requests:    {}", v["requests_total"]);
409                                        println!("  Compressed:  {}", v["requests_compressed"]);
410                                        println!("  Tokens saved: {}", v["tokens_saved"]);
411                                        println!(
412                                            "  Compression: {}%",
413                                            v["compression_ratio_pct"].as_str().unwrap_or("0.0")
414                                        );
415                                    } else {
416                                        println!("{body}");
417                                    }
418                                }
419                                Err(_) => {
420                                    println!("No proxy running on port {port}.");
421                                    println!("Start with: nebu-ctx proxy start");
422                                }
423                            }
424                        }
425                        _ => {
426                            println!("Usage: nebu-ctx proxy <start|stop|status> [--port=4444]");
427                        }
428                    }
429                    return;
430                }
431                #[cfg(not(feature = "http-server"))]
432                {
433                    eprintln!("nebu-ctx proxy is not available in this build");
434                    std::process::exit(1);
435                }
436            }
437            "init" => {
438                super::cmd_init(&rest);
439                return;
440            }
441            "setup" => {
442                let non_interactive = rest.iter().any(|a| a == "--non-interactive");
443                let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
444                let fix = rest.iter().any(|a| a == "--fix");
445                let json = rest.iter().any(|a| a == "--json");
446
447                if non_interactive || fix || json || yes {
448                    let opts = setup::SetupOptions {
449                        non_interactive,
450                        yes,
451                        fix,
452                        json,
453                    };
454                    match setup::run_setup_with_options(opts) {
455                        Ok(report) => {
456                            if json {
457                                println!(
458                                    "{}",
459                                    serde_json::to_string_pretty(&report)
460                                        .unwrap_or_else(|_| "{}".to_string())
461                                );
462                            }
463                            if !report.success {
464                                std::process::exit(1);
465                            }
466                        }
467                        Err(e) => {
468                            eprintln!("{e}");
469                            std::process::exit(1);
470                        }
471                    }
472                } else {
473                    setup::run_setup();
474                }
475                return;
476            }
477            "bootstrap" => {
478                let json = rest.iter().any(|a| a == "--json");
479                let opts = setup::SetupOptions {
480                    non_interactive: true,
481                    yes: true,
482                    fix: true,
483                    json,
484                };
485                match setup::run_setup_with_options(opts) {
486                    Ok(report) => {
487                        if json {
488                            println!(
489                                "{}",
490                                serde_json::to_string_pretty(&report)
491                                    .unwrap_or_else(|_| "{}".to_string())
492                            );
493                        }
494                        if !report.success {
495                            std::process::exit(1);
496                        }
497                    }
498                    Err(e) => {
499                        eprintln!("{e}");
500                        std::process::exit(1);
501                    }
502                }
503                return;
504            }
505            "status" => {
506                let code = status::run_cli(&rest);
507                if code != 0 {
508                    std::process::exit(code);
509                }
510                return;
511            }
512            "read" => {
513                super::cmd_read(&rest);
514                return;
515            }
516            "diff" => {
517                super::cmd_diff(&rest);
518                return;
519            }
520            "grep" => {
521                super::cmd_grep(&rest);
522                return;
523            }
524            "find" => {
525                super::cmd_find(&rest);
526                return;
527            }
528            "ls" => {
529                super::cmd_ls(&rest);
530                return;
531            }
532            "deps" => {
533                super::cmd_deps(&rest);
534                return;
535            }
536            "discover" => {
537                super::cmd_discover(&rest);
538                return;
539            }
540            "filter" => {
541                super::cmd_filter(&rest);
542                return;
543            }
544            "graph" => {
545                let mut action = "build";
546                let mut path_arg: Option<&str> = None;
547                for arg in &rest {
548                    if arg == "build" {
549                        action = "build";
550                    } else {
551                        path_arg = Some(arg.as_str());
552                    }
553                }
554                let root = path_arg
555                    .map(String::from)
556                    .or_else(|| {
557                        std::env::current_dir()
558                            .ok()
559                            .map(|p| p.to_string_lossy().to_string())
560                    })
561                    .unwrap_or_else(|| ".".to_string());
562                match action {
563                    "build" => {
564                        let index = core::graph_index::load_or_build(&root);
565                        println!(
566                            "Graph built: {} files, {} edges",
567                            index.files.len(),
568                            index.edges.len()
569                        );
570                    }
571                    _ => {
572                        eprintln!("Usage: nebu-ctx graph [build] [path]");
573                    }
574                }
575                return;
576            }
577            "session" => {
578                super::cmd_session();
579                return;
580            }
581            "wrapped" => {
582                super::cmd_wrapped(&rest);
583                return;
584            }
585            "sessions" => {
586                super::cmd_sessions(&rest);
587                return;
588            }
589            "benchmark" => {
590                super::cmd_benchmark(&rest);
591                return;
592            }
593            "config" => {
594                super::cmd_config(&rest);
595                return;
596            }
597            "cache" => {
598                super::cmd_cache(&rest);
599                return;
600            }
601            "theme" => {
602                super::cmd_theme(&rest);
603                return;
604            }
605            "tee" => {
606                super::cmd_tee(&rest);
607                return;
608            }
609            "terse" => {
610                super::cmd_terse(&rest);
611                return;
612            }
613            "slow-log" => {
614                super::cmd_slow_log(&rest);
615                return;
616            }
617
618            "doctor" => {
619                let code = doctor::run_cli(&rest);
620                if code != 0 {
621                    std::process::exit(code);
622                }
623                return;
624            }
625            "gotchas" | "bugs" => {
626                super::connect::cmd_gotchas(&rest);
627                return;
628            }
629            "buddy" | "pet" => {
630                super::connect::cmd_buddy(&rest);
631                return;
632            }
633            "hook" => {
634                let action = rest.first().map(|s| s.as_str()).unwrap_or("help");
635                match action {
636                    "rewrite" => hook_handlers::handle_rewrite(),
637                    "redirect" => hook_handlers::handle_redirect(),
638                    "copilot" => hook_handlers::handle_copilot(),
639                    "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
640                    "codex-session-start" => hook_handlers::handle_codex_session_start(),
641                    "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
642                    "stop" => hook_handlers::handle_stop(),
643                    "post-tool-use" => hook_handlers::handle_post_tool_use(),
644                    "pre-compact" => hook_handlers::handle_pre_compact(),
645                    "session-start" => hook_handlers::handle_session_start(),
646                    "user-prompt-submit" => hook_handlers::handle_user_prompt_submit(),
647                    _ => {
648                        eprintln!("Usage: nebu-ctx hook <rewrite|redirect|copilot|codex-pretooluse|codex-session-start|rewrite-inline|stop|post-tool-use|pre-compact|session-start|user-prompt-submit>");
649                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
650                        std::process::exit(1);
651                    }
652                }
653                return;
654            }
655            "report-issue" | "report" => {
656                report::run(&rest);
657                return;
658            }
659            "uninstall" => {
660                uninstall::run();
661                return;
662            }
663            "bypass" => {
664                if rest.is_empty() {
665                    eprintln!("Usage: nebu-ctx bypass \"command\"");
666                    eprintln!("Runs the command with zero compression (raw passthrough).");
667                    std::process::exit(1);
668                }
669                let command = if rest.len() == 1 {
670                    rest[0].clone()
671                } else {
672                    shell::join_command(&args[2..])
673                };
674                std::env::set_var("NEBU_CTX_RAW", "1");
675                let code = shell::exec(&command);
676                std::process::exit(code);
677            }
678            "safety-levels" | "safety" => {
679                println!("{}", core::compression_safety::format_safety_table());
680                return;
681            }
682            "cheat" | "cheatsheet" | "cheat-sheet" => {
683                super::cmd_cheatsheet();
684                return;
685            }
686            "connect" => {
687                super::connect::cmd_connect(&rest);
688                return;
689            }
690            "disconnect" => {
691                super::connect::cmd_disconnect();
692                return;
693            }
694            "--version" | "-V" => {
695                println!("nebu-ctx {}", env!("CARGO_PKG_VERSION"));
696                return;
697            }
698            "--help" | "-h" => {
699                print_help();
700                return;
701            }
702            "mcp" => {}
703            "on" => {
704                eprintln!("nebu-ctx: `nebu-ctx on` is a shell function, not a binary command.");
705                eprintln!("  Run: source ~/.nebu-ctx/shell-hook.fish  (fish)");
706                eprintln!("  Or add the shell hook to your shell profile via: nebu-ctx setup");
707                std::process::exit(1);
708            }
709            "off" => {
710                eprintln!("nebu-ctx: `nebu-ctx off` is a shell function, not a binary command.");
711                eprintln!("  Run: source ~/.nebu-ctx/shell-hook.fish  (fish)");
712                eprintln!("  Or add the shell hook to your shell profile via: nebu-ctx setup");
713                std::process::exit(1);
714            }
715            _ => {
716                eprintln!("nebu-ctx: unknown command '{}'\n", args[1]);
717                print_help();
718                std::process::exit(1);
719            }
720        }
721    }
722
723    if let Err(e) = run_mcp_server() {
724        eprintln!("nebu-ctx: {e}");
725        std::process::exit(1);
726    }
727}
728
729fn passthrough(command: &str) -> ! {
730    let (shell, flag) = shell::shell_and_flag();
731    let status = std::process::Command::new(&shell)
732        .arg(&flag)
733        .arg(command)
734        .env("NEBU_CTX_ACTIVE", "1")
735        .status()
736        .map(|s| s.code().unwrap_or(1))
737        .unwrap_or(127);
738    std::process::exit(status);
739}
740
741fn run_async<F: std::future::Future>(future: F) -> F::Output {
742    tokio::runtime::Runtime::new()
743        .expect("failed to create async runtime")
744        .block_on(future)
745}
746
747fn run_mcp_server() -> Result<()> {
748    use rmcp::ServiceExt;
749    use tracing_subscriber::EnvFilter;
750
751    std::env::set_var("NEBU_CTX_MCP_SERVER", "1");
752
753    let rt = tokio::runtime::Runtime::new()?;
754    rt.block_on(async {
755        tracing_subscriber::fmt()
756            .with_env_filter(EnvFilter::from_default_env())
757            .with_writer(std::io::stderr)
758            .init();
759
760        tracing::info!(
761            "nebu-ctx v{} MCP server starting",
762            env!("CARGO_PKG_VERSION")
763        );
764
765        let server = tools::create_server();
766        core::telemetry_queue::start_drain_task();
767        let transport =
768            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
769        let service = server.serve(transport).await?;
770        service.waiting().await?;
771
772        core::stats::flush();
773        core::mode_predictor::ModePredictor::flush();
774        core::feedback::FeedbackStore::flush();
775
776        Ok(())
777    })
778}
779
780fn print_help() {
781    println!(
782        "nebu-ctx {version} — Context Runtime for AI Agents
783
78490+ compression patterns | 46 MCP tools | Context Continuity Protocol
785
786USAGE:
787    nebu-ctx                       Start MCP server (stdio)
788    nebu-ctx serve                 Start MCP server (Streamable HTTP)
789    nebu-ctx -t \"command\"          Track command (full output + stats, no compression)
790    nebu-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
791    nebu-ctx -c --raw \"command\"    Execute without compression (full output)
792    nebu-ctx exec \"command\"        Same as -c
793    nebu-ctx bypass \"command\"      Run command with zero compression (raw passthrough)
794    nebu-ctx shell                 Interactive shell with compression
795
796COMMANDS:
797         token-report [--json]          Token + memory report (project + session + CEP)
798    gain [action]              Fetch analytics from server (report, score, cost, tasks, etc.)
799    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
800    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
801    proxy status                   Show proxy statistics
802    cache [list|clear|stats]       Show/manage file read cache
803    wrapped [--week|--month|--all] Savings report card (shareable)
804    sessions [list|show|cleanup]   Manage CCP sessions (~/.nebu-ctx/sessions/)
805    benchmark run [path] [--json]  Run real benchmark on project files
806    benchmark report [path]        Generate shareable Markdown report
807    cheatsheet                     Command cheat sheet & workflow quick reference
808    setup                          One-command setup: shell + editor + verify
809    bootstrap                      Non-interactive setup + fix (zero-config)
810    status [--json]                Show setup + MCP + rules status
811    init <shell>                   Print shell hook to stdout (eval pattern, like starship)
812                                   Supported: bash, zsh, fish, powershell
813    init --global                  Install shell aliases to rc file (file-based)
814    init --agent <name>            Configure MCP for specific editor/agent
815    read <file> [-m mode]          Read file with compression
816    diff <file1> <file2>           Compressed file diff
817    grep <pattern> [path]          Search with compressed output
818    find <pattern> [path]          Find files with compressed output
819    ls [path]                      Directory listing with compression
820    deps [path]                    Show project dependencies
821    discover                       Find uncompressed commands in shell history
822    filter [list|validate|init]    Manage custom compression filters (~/.nebu-ctx/filters/)
823    session                        Show adoption statistics
824    config                         Show/edit configuration (~/.nebu-ctx/config.toml)
825    theme [list|set|export|import] Customize terminal colors and themes
826    tee [list|clear|show <file>|last] Manage output tee files (~/.nebu-ctx/tee/)
827    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
828    slow-log [list|clear]          Show/clear slow command log (~/.nebu-ctx/slow-commands.log)
829    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
830    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
831    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
832    safety-levels                  Show compression safety levels per command
833    bypass \"command\"               Run command with zero compression (raw passthrough)
834    uninstall                      Remove shell hook, MCP configs, and data directory
835
836SHELL HOOK PATTERNS (90+):
837    git       status, log, diff, add, commit, push, pull, fetch, clone,
838              branch, checkout, switch, merge, stash, tag, reset, remote
839    docker    build, ps, images, logs, compose, exec, network
840    npm/pnpm  install, test, run, list, outdated, audit
841    cargo     build, test, check, clippy
842    gh        pr list/view/create, issue list/view, run list/view
843    kubectl   get pods/services/deployments, logs, describe, apply
844    python    pip install/list/outdated, ruff check/format, poetry, uv
845    linters   eslint, biome, prettier, golangci-lint
846    builds    tsc, next build, vite build
847    ruby      rubocop, bundle install/update, rake test, rails test
848    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
849    iac       terraform, make, maven, gradle, dotnet, flutter, dart
850    utils     curl, grep/rg, find, ls, wget, env
851    data      JSON schema extraction, log deduplication
852
853READ MODES:
854    auto                           Auto-select optimal mode (default)
855    full                           Full content (cached re-reads = 13 tokens)
856    map                            Dependency graph + API signatures
857    signatures                     tree-sitter AST extraction (18 languages)
858    task                           Task-relevant filtering (requires ctx_session task)
859    reference                      One-line reference stub (cheap cache key)
860    aggressive                     Syntax-stripped content
861    entropy                        Shannon entropy filtered
862    diff                           Changed lines only
863    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
864
865ENVIRONMENT:
866    NEBU_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
867    NEBU_CTX_ENABLED=0             Prevent shell hook auto-start (nebu-ctx-on still works)
868    NEBU_CTX_RAW=1                 Same as --raw for current command
869    NEBU_CTX_AUTONOMY=false        Disable autonomous features
870    NEBU_CTX_COMPRESS=1            Force compression (even for excluded commands)
871
872OPTIONS:
873    --version, -V                  Show version
874    --help, -h                     Show this help
875
876EXAMPLES:
877        nebu-ctx -c \"git status\"       Compressed git output
878        nebu-ctx -c \"kubectl get pods\" Compressed k8s output
879        nebu-ctx -c \"gh pr list\"       Compressed GitHub CLI output
880            nebu-ctx token-report --json   Machine-readable token + memory report
881        nebu-ctx wrapped               Weekly savings report card
882        nebu-ctx wrapped --month       Monthly savings report card
883        nebu-ctx sessions list         List all CCP sessions
884        nebu-ctx sessions show         Show latest session state
885        nebu-ctx discover              Find missed savings in shell history
886        nebu-ctx setup                 One-command setup (shell + editors + verify)
887        nebu-ctx bootstrap             Non-interactive setup + fix (zero-config)
888        nebu-ctx bootstrap --json      Machine-readable bootstrap report
889        nebu-ctx init --global         Install shell aliases (file-based, includes nebu-ctx-on/off)
890
891EVAL INIT (starship/zoxide style — always in sync with binary version):
892    # bash: add to ~/.bashrc
893    eval \"$(nebu-ctx init bash)\"
894    # zsh: add to ~/.zshrc
895    eval \"$(nebu-ctx init zsh)\"
896    # fish: add to ~/.config/fish/config.fish
897    nebu-ctx init fish | source
898    # powershell: add to $PROFILE
899    nebu-ctx init powershell | Invoke-Expression
900    nebu-ctx-on                    Enable shell aliases in track mode (full output + stats)
901    nebu-ctx-off                   Disable all shell aliases
902    nebu-ctx-mode track            Track mode: full output, stats recorded (default)
903    nebu-ctx-mode compress         Compress mode: all output compressed (power users)
904    nebu-ctx-mode off              Same as nebu-ctx-off
905    nebu-ctx-status                Show whether compression is active
906    nebu-ctx init --agent pi       Install Pi Coding Agent extension
907    nebu-ctx doctor                Check PATH, config, MCP, and local edge health
908    nebu-ctx doctor --fix --json   Repair + machine-readable report
909    nebu-ctx status --json         Machine-readable current status
910    nebu-ctx read src/main.rs -m map
911    nebu-ctx grep \"pub fn\" src/
912    nebu-ctx deps .
913
914HOST CONNECTION:
915    connect [--endpoint <url>] [--token <token>]  Save and validate a server connection
916    status                         Includes host connection status
917    disconnect                     Remove the saved server connection
918
919TROUBLESHOOTING:
920    Commands broken?     nebu-ctx-off             (fixes current session)
921    Permanent fix?       nebu-ctx uninstall       (removes all hooks)
922    Manual fix?          Edit ~/.zshrc, remove the \"nebu-ctx shell hook\" block
923    Binary missing?      Aliases auto-fallback to original commands (safe)
924    Preview init?        nebu-ctx init --global --dry-run
925
926WEBSITE: https://nebu-ctx.com
927GITHUB:  https://github.com/MarkBovee/nebu-ctx
928",
929        version = env!("CARGO_PKG_VERSION"),
930    );
931}