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                    "telemetry" => {
648                        let tool_name = rest.get(1).cloned().unwrap_or_else(|| "unknown".to_string());
649                        let tokens_original: i64 = rest.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
650                        let tokens_saved: i64 = rest.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
651                        core::telemetry_queue::fire_sync(crate::models::TelemetryIngestRequest {
652                            tool_name: core::stats::normalize_command(&tool_name),
653                            tokens_original,
654                            tokens_saved,
655                            duration_ms: 0,
656                            mode: Some("plugin".to_string()),
657                            repository_fingerprint: None,
658                            checkout_binding: None,
659                            project_slug: None,
660                        });
661                    }
662                    _ => {
663                        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|telemetry>");
664                        eprintln!("  Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)");
665                        std::process::exit(1);
666                    }
667                }
668                return;
669            }
670            "report-issue" | "report" => {
671                report::run(&rest);
672                return;
673            }
674            "uninstall" => {
675                uninstall::run();
676                return;
677            }
678            "bypass" => {
679                if rest.is_empty() {
680                    eprintln!("Usage: nebu-ctx bypass \"command\"");
681                    eprintln!("Runs the command with zero compression (raw passthrough).");
682                    std::process::exit(1);
683                }
684                let command = if rest.len() == 1 {
685                    rest[0].clone()
686                } else {
687                    shell::join_command(&args[2..])
688                };
689                std::env::set_var("NEBU_CTX_RAW", "1");
690                let code = shell::exec(&command);
691                std::process::exit(code);
692            }
693            "safety-levels" | "safety" => {
694                println!("{}", core::compression_safety::format_safety_table());
695                return;
696            }
697            "cheat" | "cheatsheet" | "cheat-sheet" => {
698                super::cmd_cheatsheet();
699                return;
700            }
701            "connect" => {
702                super::connect::cmd_connect(&rest);
703                return;
704            }
705            "disconnect" => {
706                super::connect::cmd_disconnect();
707                return;
708            }
709            "--version" | "-V" => {
710                println!("nebu-ctx {}", env!("CARGO_PKG_VERSION"));
711                return;
712            }
713            "--help" | "-h" => {
714                print_help();
715                return;
716            }
717            "mcp" => {}
718            "on" => {
719                eprintln!("nebu-ctx: `nebu-ctx on` is a shell function, not a binary command.");
720                eprintln!("  Run: source ~/.nebu-ctx/shell-hook.fish  (fish)");
721                eprintln!("  Or add the shell hook to your shell profile via: nebu-ctx setup");
722                std::process::exit(1);
723            }
724            "off" => {
725                eprintln!("nebu-ctx: `nebu-ctx off` is a shell function, not a binary command.");
726                eprintln!("  Run: source ~/.nebu-ctx/shell-hook.fish  (fish)");
727                eprintln!("  Or add the shell hook to your shell profile via: nebu-ctx setup");
728                std::process::exit(1);
729            }
730            _ => {
731                eprintln!("nebu-ctx: unknown command '{}'\n", args[1]);
732                print_help();
733                std::process::exit(1);
734            }
735        }
736    }
737
738    if let Err(e) = run_mcp_server() {
739        eprintln!("nebu-ctx: {e}");
740        std::process::exit(1);
741    }
742}
743
744fn passthrough(command: &str) -> ! {
745    let (shell, flag) = shell::shell_and_flag();
746    let status = std::process::Command::new(&shell)
747        .arg(&flag)
748        .arg(command)
749        .env("NEBU_CTX_ACTIVE", "1")
750        .status()
751        .map(|s| s.code().unwrap_or(1))
752        .unwrap_or(127);
753    std::process::exit(status);
754}
755
756#[cfg(feature = "http-server")]
757fn run_async<F: std::future::Future>(future: F) -> F::Output {
758    tokio::runtime::Runtime::new()
759        .expect("failed to create async runtime")
760        .block_on(future)
761}
762
763fn run_mcp_server() -> Result<()> {
764    use rmcp::ServiceExt;
765    use tracing_subscriber::EnvFilter;
766
767    std::env::set_var("NEBU_CTX_MCP_SERVER", "1");
768
769    let rt = tokio::runtime::Runtime::new()?;
770    rt.block_on(async {
771        tracing_subscriber::fmt()
772            .with_env_filter(EnvFilter::from_default_env())
773            .with_writer(std::io::stderr)
774            .init();
775
776        tracing::info!(
777            "nebu-ctx v{} MCP server starting",
778            env!("CARGO_PKG_VERSION")
779        );
780
781        let server = tools::create_server();
782        core::telemetry_queue::start_drain_task();
783        let transport =
784            mcp_stdio::HybridStdioTransport::new_server(tokio::io::stdin(), tokio::io::stdout());
785        let service = server.serve(transport).await?;
786        service.waiting().await?;
787
788        core::stats::flush();
789        core::mode_predictor::ModePredictor::flush();
790        core::feedback::FeedbackStore::flush();
791
792        Ok(())
793    })
794}
795
796fn print_help() {
797    println!(
798        "nebu-ctx {version} — Context Runtime for AI Agents
799
80090+ compression patterns | 46 MCP tools | Context Continuity Protocol
801
802USAGE:
803    nebu-ctx                       Start MCP server (stdio)
804    nebu-ctx serve                 Start MCP server (Streamable HTTP)
805    nebu-ctx -t \"command\"          Track command (full output + stats, no compression)
806    nebu-ctx -c \"command\"          Execute with compressed output (used by AI hooks)
807    nebu-ctx -c --raw \"command\"    Execute without compression (full output)
808    nebu-ctx exec \"command\"        Same as -c
809    nebu-ctx bypass \"command\"      Run command with zero compression (raw passthrough)
810    nebu-ctx shell                 Interactive shell with compression
811
812COMMANDS:
813         token-report [--json]          Token + memory report (project + session + CEP)
814    gain [action]              Fetch analytics from server (report, score, cost, tasks, etc.)
815    serve [--host H] [--port N]    MCP over HTTP (Streamable HTTP, local-first)
816    proxy start [--port=4444]      API proxy: compress tool_results before LLM API
817    proxy status                   Show proxy statistics
818    cache [list|clear|stats]       Show/manage file read cache
819    wrapped [--week|--month|--all] Savings report card (shareable)
820    sessions [list|show|cleanup]   Manage CCP sessions (~/.nebu-ctx/sessions/)
821    benchmark run [path] [--json]  Run real benchmark on project files
822    benchmark report [path]        Generate shareable Markdown report
823    cheatsheet                     Command cheat sheet & workflow quick reference
824    setup                          One-command setup: shell + editor + verify
825    bootstrap                      Non-interactive setup + fix (zero-config)
826    status [--json]                Show setup + MCP + rules status
827    init <shell>                   Print shell hook to stdout (eval pattern, like starship)
828                                   Supported: bash, zsh, fish, powershell
829    init --global                  Install shell aliases to rc file (file-based)
830    init --agent <name>            Configure MCP for specific editor/agent
831    read <file> [-m mode]          Read file with compression
832    diff <file1> <file2>           Compressed file diff
833    grep <pattern> [path]          Search with compressed output
834    find <pattern> [path]          Find files with compressed output
835    ls [path]                      Directory listing with compression
836    deps [path]                    Show project dependencies
837    discover                       Find uncompressed commands in shell history
838    filter [list|validate|init]    Manage custom compression filters (~/.nebu-ctx/filters/)
839    session                        Show adoption statistics
840    config                         Show/edit configuration (~/.nebu-ctx/config.toml)
841    theme [list|set|export|import] Customize terminal colors and themes
842    tee [list|clear|show <file>|last] Manage output tee files (~/.nebu-ctx/tee/)
843    terse [off|lite|full|ultra]    Set agent output verbosity (saves 25-65% output tokens)
844    slow-log [list|clear]          Show/clear slow command log (~/.nebu-ctx/slow-commands.log)
845    gotchas [list|clear|export|stats] Bug Memory: view/manage auto-detected error patterns
846    buddy [show|stats|ascii|json]  Token Guardian: your data-driven coding companion
847    doctor [--fix] [--json]        Run diagnostics (and optionally repair)
848    safety-levels                  Show compression safety levels per command
849    bypass \"command\"               Run command with zero compression (raw passthrough)
850    uninstall                      Remove shell hook, MCP configs, and data directory
851
852SHELL HOOK PATTERNS (90+):
853    git       status, log, diff, add, commit, push, pull, fetch, clone,
854              branch, checkout, switch, merge, stash, tag, reset, remote
855    docker    build, ps, images, logs, compose, exec, network
856    npm/pnpm  install, test, run, list, outdated, audit
857    cargo     build, test, check, clippy
858    gh        pr list/view/create, issue list/view, run list/view
859    kubectl   get pods/services/deployments, logs, describe, apply
860    python    pip install/list/outdated, ruff check/format, poetry, uv
861    linters   eslint, biome, prettier, golangci-lint
862    builds    tsc, next build, vite build
863    ruby      rubocop, bundle install/update, rake test, rails test
864    tests     jest, vitest, pytest, go test, playwright, rspec, minitest
865    iac       terraform, make, maven, gradle, dotnet, flutter, dart
866    utils     curl, grep/rg, find, ls, wget, env
867    data      JSON schema extraction, log deduplication
868
869READ MODES:
870    auto                           Auto-select optimal mode (default)
871    full                           Full content (cached re-reads = 13 tokens)
872    map                            Dependency graph + API signatures
873    signatures                     tree-sitter AST extraction (18 languages)
874    task                           Task-relevant filtering (requires ctx_session task)
875    reference                      One-line reference stub (cheap cache key)
876    aggressive                     Syntax-stripped content
877    entropy                        Shannon entropy filtered
878    diff                           Changed lines only
879    lines:N-M                      Specific line ranges (e.g. lines:10-50,80)
880
881ENVIRONMENT:
882    NEBU_CTX_DISABLED=1            Bypass ALL compression + prevent shell hook from loading
883    NEBU_CTX_ENABLED=0             Prevent shell hook auto-start (nebu-ctx-on still works)
884    NEBU_CTX_RAW=1                 Same as --raw for current command
885    NEBU_CTX_AUTONOMY=false        Disable autonomous features
886    NEBU_CTX_COMPRESS=1            Force compression (even for excluded commands)
887
888OPTIONS:
889    --version, -V                  Show version
890    --help, -h                     Show this help
891
892EXAMPLES:
893        nebu-ctx -c \"git status\"       Compressed git output
894        nebu-ctx -c \"kubectl get pods\" Compressed k8s output
895        nebu-ctx -c \"gh pr list\"       Compressed GitHub CLI output
896            nebu-ctx token-report --json   Machine-readable token + memory report
897        nebu-ctx wrapped               Weekly savings report card
898        nebu-ctx wrapped --month       Monthly savings report card
899        nebu-ctx sessions list         List all CCP sessions
900        nebu-ctx sessions show         Show latest session state
901        nebu-ctx discover              Find missed savings in shell history
902        nebu-ctx setup                 One-command setup (shell + editors + verify)
903        nebu-ctx bootstrap             Non-interactive setup + fix (zero-config)
904        nebu-ctx bootstrap --json      Machine-readable bootstrap report
905        nebu-ctx init --global         Install shell aliases (file-based, includes nebu-ctx-on/off)
906
907EVAL INIT (starship/zoxide style — always in sync with binary version):
908    # bash: add to ~/.bashrc
909    eval \"$(nebu-ctx init bash)\"
910    # zsh: add to ~/.zshrc
911    eval \"$(nebu-ctx init zsh)\"
912    # fish: add to ~/.config/fish/config.fish
913    nebu-ctx init fish | source
914    # powershell: add to $PROFILE
915    nebu-ctx init powershell | Invoke-Expression
916    nebu-ctx-on                    Enable shell aliases in track mode (full output + stats)
917    nebu-ctx-off                   Disable all shell aliases
918    nebu-ctx-mode track            Track mode: full output, stats recorded (default)
919    nebu-ctx-mode compress         Compress mode: all output compressed (power users)
920    nebu-ctx-mode off              Same as nebu-ctx-off
921    nebu-ctx-status                Show whether compression is active
922    nebu-ctx init --agent pi       Install Pi Coding Agent extension
923    nebu-ctx doctor                Check PATH, config, MCP, and local edge health
924    nebu-ctx doctor --fix --json   Repair + machine-readable report
925    nebu-ctx status --json         Machine-readable current status
926    nebu-ctx read src/main.rs -m map
927    nebu-ctx grep \"pub fn\" src/
928    nebu-ctx deps .
929
930HOST CONNECTION:
931    connect [--endpoint <url>] [--token <token>]  Save and validate a server connection
932    status                         Includes host connection status
933    disconnect                     Remove the saved server connection
934
935TROUBLESHOOTING:
936    Commands broken?     nebu-ctx-off             (fixes current session)
937    Permanent fix?       nebu-ctx uninstall       (removes all hooks)
938    Manual fix?          Edit ~/.zshrc, remove the \"nebu-ctx shell hook\" block
939    Binary missing?      Aliases auto-fallback to original commands (safe)
940    Preview init?        nebu-ctx init --global --dry-run
941
942WEBSITE: https://nebu-ctx.com
943GITHUB:  https://github.com/MarkBovee/nebu-ctx
944",
945        version = env!("CARGO_PKG_VERSION"),
946    );
947}