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