1mod args;
4#[cfg(not(target_arch = "wasm32"))]
5pub(crate) mod banner;
6pub mod client_startup;
7pub mod crash_restore;
8pub mod easy_startup;
9pub mod input_state;
10pub mod management;
11pub mod model_select;
12#[cfg(not(target_arch = "wasm32"))]
13mod non_interactive;
14pub mod server_startup;
15#[cfg(not(target_arch = "wasm32"))]
16mod setup;
17mod startup_guard;
18mod tasks;
19#[cfg(not(target_arch = "wasm32"))]
20mod terminal_cleanup;
21pub mod theme;
22
23pub use tasks::{execute_due_tasks_owned_public, execute_due_tasks_public};
27
28use anyhow::Result;
29pub use args::{Args, MCP_TOKEN_ENV};
30use clap::Parser;
31use tracing::debug;
32
33use crate::events::EventHandler;
34use crate::llm::OllamaClient;
35use crate::settings::Settings;
36use crate::state::app_state::AppState;
37use crate::ui::App;
38
39#[cfg(not(target_arch = "wasm32"))]
41pub fn create_llm_client(args: &Args) -> Result<OllamaClient> {
42 let mut client = create_llm_client_inner(args)?;
43 if let Some(secs) = args.llm_request_timeout {
44 client = client.with_request_timeout(std::time::Duration::from_secs(secs));
45 }
46 if let Some(max_tokens) = args.llm_max_tokens {
47 client = client.with_max_tokens(max_tokens);
48 }
49 client = client.with_sampling(crate::llm::SamplingOptions {
50 seed: args.llm_seed,
51 temperature: args.llm_temperature,
52 });
53 Ok(client)
54}
55
56#[cfg(not(target_arch = "wasm32"))]
57fn create_llm_client_inner(args: &Args) -> Result<OllamaClient> {
58 if let Some(ref openai_url) = args.openai_url {
59 let api_key = args.resolve_api_key().ok_or_else(|| {
60 anyhow::anyhow!(
61 "API key required for OpenAI-compatible endpoint.\n Set NETGET_API_KEY (or OPENAI_API_KEY); --api-key also works but exposes the key in the process table."
62 )
63 })?;
64 if args.model.is_none() {
65 anyhow::bail!(
66 "--model is required when using --openai-url.\n Example: --openai-url {} --model gpt-4o",
67 openai_url
68 );
69 }
70 Ok(OllamaClient::new_openai(openai_url, api_key))
71 } else {
72 let ollama_url = args
73 .ollama_url
74 .as_deref()
75 .unwrap_or("http://localhost:11434");
76 Ok(OllamaClient::new(ollama_url))
77 }
78}
79
80#[cfg(not(target_arch = "wasm32"))]
82pub async fn run() -> Result<()> {
83 let args = Args::parse();
84
85 if args.simple_list {
87 use crate::protocol::EASY_REGISTRY;
88 println!("Available simple protocols:");
89 println!();
90 let protocols = EASY_REGISTRY.get_all_names();
91 if protocols.is_empty() {
92 println!(" No simple protocols available (check compiled features)");
93 } else {
94 for name in protocols {
95 println!(" - {}", name);
96 }
97 }
98 println!();
99 println!("Usage: netget --simple <protocol>");
100 println!("Example: netget --simple http");
101 return Ok(());
102 }
103
104 if let Some(ref protocol) = args.simple_protocol {
106 return run_simple_protocol(protocol, &args).await;
107 }
108
109 if args.client_list {
111 println!("Available client protocols:");
112 println!();
113 let mut protocols = crate::protocol::CLIENT_REGISTRY.list_protocols();
114 protocols.sort();
115 if protocols.is_empty() {
116 println!(" No client protocols available (check compiled features)");
117 } else {
118 for name in protocols {
119 println!(" - {}", name);
120 }
121 }
122 println!();
123 println!("Usage: netget --client <protocol> --connect <address> [instruction]");
124 println!("Example: netget --client redis --connect 127.0.0.1:6379");
125 return Ok(());
126 }
127
128 if args.docs {
130 print!("{}", crate::protocol::render_all_protocol_docs());
131 return Ok(());
132 }
133
134 if let Some(ref protocol) = args.client_protocol {
136 return run_client(protocol, &args).await;
137 }
138
139 if let Some(ref protocol) = args.server_protocol {
142 return run_server_direct(protocol, &args).await;
143 }
144
145 #[cfg(feature = "mcp-stdio")]
147 if args.mcp_stdio {
148 setup::init_logging(&args, false)?;
155 let settings = Settings::load();
156 return crate::mcp_stdio::run_mcp_stdio(&args, settings).await;
157 }
158 #[cfg(not(feature = "mcp-stdio"))]
159 if args.mcp_stdio {
160 anyhow::bail!(
161 "MCP STDIO mode requires the 'mcp-stdio' feature.\n\
162 Build with: cargo build --features mcp-stdio"
163 );
164 }
165
166 #[cfg(feature = "mcp-http")]
168 if let Some(port) = args.mcp_http {
169 setup::init_logging(&args, false)?;
172 let settings = Settings::load();
173 return crate::mcp_stdio::run_mcp_http(&args, settings, port).await;
174 }
175 #[cfg(not(feature = "mcp-http"))]
176 if args.mcp_http.is_some() {
177 anyhow::bail!(
178 "MCP HTTP mode requires the 'mcp-http' feature.\n\
179 Build with: cargo build --features mcp-http"
180 );
181 }
182
183 let actions_json = args.get_actions_json()?;
185
186 let prompt = args.get_prompt()?;
188
189 let is_interactive = prompt.is_none() && actions_json.is_none() && args.is_interactive();
191
192 setup::init_logging(&args, is_interactive)?;
194
195 let settings = Settings::load();
197
198 if let Some(actions) = actions_json {
200 non_interactive::run_with_actions(actions, &args, settings).await
202 } else if let Some(prompt) = prompt {
203 non_interactive::run_non_interactive(prompt, &args, settings).await
205 } else if args.is_interactive() {
206 debug!("Entering interactive TUI mode");
208 debug!("Creating AppState...");
209 let base_url = args
210 .openai_url
211 .clone()
212 .or_else(|| args.ollama_url.clone())
213 .unwrap_or_else(|| "http://localhost:11434".to_string());
214 let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
215 state.set_min_stability(args.parse_min_stability()?).await;
216 debug!("AppState created");
217
218 debug!("Configuring rate limiter...");
220 let rate_limiter_config = args.build_rate_limiter_config();
221 state.configure_rate_limiter(rate_limiter_config).await?;
222 debug!("Rate limiter configured");
223
224 debug!("Parsing scripting mode...");
226 let mode_to_set = if let Some(mode) = args.parse_scripting_mode()? {
227 Some(mode)
228 } else {
229 settings.parse_scripting_mode()
230 };
231 debug!("Scripting mode to set: {:?}", mode_to_set);
232
233 if let Some(mode) = mode_to_set {
234 debug!("Getting scripting environment for validation...");
236 let scripting_env = state.get_scripting_env().await;
237 debug!("Scripting environment retrieved");
238 let available = match mode {
239 crate::state::app_state::ScriptingMode::On => true, crate::state::app_state::ScriptingMode::Off => true, crate::state::app_state::ScriptingMode::Python => scripting_env.python.is_some(),
242 crate::state::app_state::ScriptingMode::JavaScript => {
243 scripting_env.javascript.is_some()
244 }
245 crate::state::app_state::ScriptingMode::Go => scripting_env.go.is_some(),
246 crate::state::app_state::ScriptingMode::Perl => scripting_env.perl.is_some(),
247 };
248
249 if !available {
250 anyhow::bail!(
251 "{} environment is not available on this system. Please install it or choose a different environment.",
252 mode
253 );
254 }
255
256 state.set_selected_scripting_mode(mode).await;
257 }
258
259 debug!("Parsing theme argument: {}", args.theme);
261 let theme_option = theme::parse_theme(&args.theme)?;
262 debug!("Theme option parsed: {:?}", theme_option);
263 let theme = if let Some(t) = theme_option {
264 debug!("Using explicit theme: {:?}", t);
265 t
266 } else {
267 debug!("Auto-detecting theme...");
269 let detected = theme::detect_theme().unwrap_or(theme::Theme::Neutral);
270 debug!("Theme detected: {:?}", detected);
271 detected
272 };
273 debug!("Creating color palette from theme: {:?}", theme);
274 let color_palette = theme::ColorPalette::from_theme(theme);
275 debug!("Color palette created");
276
277 debug!("Getting system capabilities...");
279 let system_capabilities = state.get_system_capabilities().await;
280 debug!("Creating App...");
281 let app = App::new(system_capabilities);
282
283 #[cfg(feature = "embedded-llm")]
285 let llm = {
286 if args.openai_url.is_some() {
287 debug!("Creating OpenAI-compatible client...");
288 create_llm_client(&args)?
289 .with_mock_config_file(args.mock_config_file.clone())
290 .with_app_state(state.clone())
291 } else {
292 let use_hybrid = args.use_embedded || args.embedded_model.is_some();
294
295 if use_hybrid {
296 debug!("Creating HybridLLMManager...");
297 let embedded_path = args
298 .embedded_model
299 .as_ref()
300 .map(|p| p.display().to_string());
301 let hybrid =
302 crate::llm::HybridLLMManager::new(args.use_embedded, embedded_path).await?;
303
304 if let Some(client) = hybrid.ollama_client().await {
305 debug!("Using Ollama backend from HybridLLMManager");
306 client
307 .with_mock_config_file(args.mock_config_file.clone())
308 .with_app_state(state.clone())
309 } else {
310 debug!("Using embedded backend - creating fallback OllamaClient");
311 let ollama_url = args
312 .ollama_url
313 .as_deref()
314 .unwrap_or("http://localhost:11434");
315 OllamaClient::new(ollama_url)
316 .with_mock_config_file(args.mock_config_file.clone())
317 .with_app_state(state.clone())
318 }
319 } else {
320 debug!("Creating OllamaClient...");
321 create_llm_client(&args)?
322 .with_mock_config_file(args.mock_config_file.clone())
323 .with_app_state(state.clone())
324 }
325 }
326 };
327
328 #[cfg(not(feature = "embedded-llm"))]
329 let llm = {
330 debug!("Creating LLM client...");
331 create_llm_client(&args)?
332 .with_mock_config_file(args.mock_config_file.clone())
333 .with_app_state(state.clone())
334 };
335
336 state.set_llm_client(llm.clone()).await;
338
339 debug!("Creating EventHandler...");
340 let event_handler = EventHandler::new(state.clone(), llm.clone());
341
342 debug!("Entering dashboard TUI...");
344 crate::tui::run_dashboard(
345 state,
346 app,
347 event_handler,
348 llm,
349 settings,
350 &args,
351 color_palette,
352 )
353 .await
354 } else {
355 anyhow::bail!(
357 "Cannot start in interactive mode without a terminal.\n\
358 Please provide a prompt via arguments or stdin."
359 )
360 }
361}
362
363#[cfg(not(target_arch = "wasm32"))]
377async fn run_client(protocol: &str, args: &Args) -> Result<()> {
378 use std::sync::Arc;
379 use std::time::Duration;
380 use tokio::sync::Mutex;
381
382 setup::init_logging(args, false)?;
384 let limits = non_interactive::RunLimits::from_args(args);
385
386 let remote_addr = args.client_addr.clone().ok_or_else(|| {
387 anyhow::anyhow!(
388 "--client requires --connect <ADDRESS>\n Example: netget --client {} --connect 127.0.0.1:6379",
389 protocol
390 )
391 })?;
392
393 let canonical = client_startup::resolve_client_protocol(protocol).ok_or_else(|| {
396 let mut available = crate::protocol::CLIENT_REGISTRY.list_protocols();
397 available.sort();
398 anyhow::anyhow!(
399 "Unknown client protocol: {}\n Available in this build: {}\n (see --client-list)",
400 protocol,
401 if available.is_empty() {
402 "none".to_string()
403 } else {
404 available.join(", ")
405 }
406 )
407 })?;
408
409 let startup_params = args.parse_client_params()?;
410 let event_handlers = args.parse_client_handlers()?;
411 let instruction = args.client_instruction(&canonical, &remote_addr);
412
413 let settings = Settings::load();
414
415 let base_url = args
417 .openai_url
418 .clone()
419 .or_else(|| args.ollama_url.clone())
420 .unwrap_or_else(|| "http://localhost:11434".to_string());
421 let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
422 state.set_min_stability(args.parse_min_stability()?).await;
423
424 state
425 .configure_rate_limiter(args.build_rate_limiter_config())
426 .await?;
427
428 if let Some(model) = args.model.clone().or_else(|| settings.model.clone()) {
429 state.set_ollama_model(Some(model)).await;
430 }
431
432 if let Some(mode) = args
433 .parse_scripting_mode()?
434 .or_else(|| settings.parse_scripting_mode())
435 {
436 state.set_selected_scripting_mode(mode).await;
437 }
438
439 if let Some(handler_mode) = args.parse_event_handler_mode()? {
440 state.set_event_handler_mode(handler_mode).await;
441 }
442
443 let mut web_search_mode = settings.get_web_search_mode();
445 if web_search_mode == crate::state::app_state::WebSearchMode::Ask {
446 web_search_mode = crate::state::app_state::WebSearchMode::Off;
447 }
448 state.set_web_search_mode(web_search_mode).await;
449
450 let llm = create_llm_client(args)?
451 .with_mock_config_file(args.mock_config_file.clone())
452 .with_app_state(state.clone());
453 state.set_llm_client(llm.clone()).await;
454
455 println!(
456 "[CLIENT] Connecting {} client to {}",
457 canonical, remote_addr
458 );
459
460 let client_id = client_startup::start_client_from_action(
461 &state,
462 &canonical,
463 &remote_addr,
464 instruction,
465 startup_params,
466 None, event_handlers,
468 None, None, llm,
471 None, )
473 .await?;
474
475 println!(
476 "[CLIENT] Client #{} ({}) connected to {}. Press Ctrl+C to stop.",
477 client_id.as_u32(),
478 canonical,
479 remote_addr
480 );
481
482 let shutdown = Arc::new(Mutex::new(false));
484 let shutdown_clone = shutdown.clone();
485 tokio::spawn(async move {
486 tokio::signal::ctrl_c().await.ok();
487 let mut shutdown = shutdown_clone.lock().await;
488 *shutdown = true;
489 });
490
491 loop {
493 tokio::time::sleep(Duration::from_millis(100)).await;
494 if *shutdown.lock().await {
495 println!("\n[CLIENT] Shutting down...");
496 break;
497 }
498 if let Some(reason) = limits.reached(&state).await {
499 println!("[CLIENT] Stopping: {reason}.");
500 break;
501 }
502 match state.get_client(client_id).await {
503 Some(client) => {
504 if matches!(
505 client.status,
506 crate::state::client::ClientStatus::Disconnected
507 | crate::state::client::ClientStatus::Error(_)
508 ) {
509 println!(
510 "[CLIENT] Client #{} is {:?}",
511 client_id.as_u32(),
512 client.status
513 );
514 break;
515 }
516 }
517 None => break,
518 }
519 }
520
521 println!("[CLIENT] Client stopped.");
522 Ok(())
523}
524
525#[cfg(not(target_arch = "wasm32"))]
537async fn run_server_direct(protocol: &str, args: &Args) -> Result<()> {
538 use tokio::sync::mpsc;
539
540 setup::init_logging(args, false)?;
541 let limits = non_interactive::RunLimits::from_args(args);
542
543 let canonical = crate::protocol::server_registry::registry()
546 .resolve(protocol)
547 .map(|p| p.protocol_name().to_string())
548 .map_err(|e| anyhow::anyhow!("{e}"))?;
549
550 let to_stderr = non_interactive::protocol_owns_stdout(&canonical);
555
556 let startup_params = args.parse_server_params()?;
557 let instruction = args.server_instruction(&canonical);
558 let settings = Settings::load();
559
560 let base_url = args
561 .openai_url
562 .clone()
563 .or_else(|| args.ollama_url.clone())
564 .unwrap_or_else(|| "http://localhost:11434".to_string());
565 let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
566 state.set_min_stability(args.parse_min_stability()?).await;
567 state
568 .configure_rate_limiter(args.build_rate_limiter_config())
569 .await?;
570
571 let configured_model = args.model.clone().or_else(|| settings.model.clone());
578 let selected_model = if args.openai_url.is_some() {
579 configured_model
580 .ok_or_else(|| anyhow::anyhow!("--model is required when using --openai-url"))?
581 } else {
582 let ollama_url = args
583 .ollama_url
584 .as_deref()
585 .unwrap_or("http://localhost:11434");
586 crate::llm::select_or_validate_model(configured_model, false, ollama_url)
587 .await?
588 .ok_or_else(|| {
589 anyhow::anyhow!("No model available — pull one in Ollama or pass --model")
590 })?
591 };
592 non_interactive::emit_status_line(
593 &format!("[SERVER] Using model: {}", selected_model),
594 to_stderr,
595 );
596 state.set_ollama_model(Some(selected_model)).await;
597
598 if let Some(mode) = args
599 .parse_scripting_mode()?
600 .or_else(|| settings.parse_scripting_mode())
601 {
602 state.set_selected_scripting_mode(mode).await;
603 }
604 if let Some(handler_mode) = args.parse_event_handler_mode()? {
605 state.set_event_handler_mode(handler_mode).await;
606 }
607 let mut web_search_mode = settings.get_web_search_mode();
608 if web_search_mode == crate::state::app_state::WebSearchMode::Ask {
609 web_search_mode = crate::state::app_state::WebSearchMode::Off;
610 }
611 state.set_web_search_mode(web_search_mode).await;
612
613 let llm = create_llm_client(args)?
614 .with_mock_config_file(args.mock_config_file.clone())
615 .with_app_state(state.clone());
616 state.set_llm_client(llm.clone()).await;
617
618 let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
622 let _forwarder = tokio::spawn(async move {
623 while let Some(msg) = status_rx.recv().await {
624 if !msg.starts_with("__") {
625 let clean = msg
626 .strip_prefix("[INFO] ")
627 .or_else(|| msg.strip_prefix("[ERROR] "))
628 .or_else(|| msg.strip_prefix("[WARN] "))
629 .or_else(|| msg.strip_prefix("[DEBUG] "))
630 .unwrap_or(&msg);
631 non_interactive::emit_status_line(clean, to_stderr);
634 }
635 }
636 });
637 tokio::task::yield_now().await;
638
639 let server_id = crate::cli::server_startup::start_server_from_action(
640 &state,
641 None, None, None, args.server_port,
645 &canonical,
646 false, None, instruction,
649 startup_params,
650 None, None, None, status_tx,
654 )
655 .await?;
656
657 let started_line = format!(
658 "Server #{} ({}) started, skipping the initial model call. Press Ctrl+C to stop.",
659 server_id.as_u32(),
660 canonical
661 );
662 non_interactive::emit_status_line(&started_line, to_stderr);
663 non_interactive::emit_status_line("Waiting for connections...\n", to_stderr);
664
665 let (_srv_tx, srv_rx) = mpsc::unbounded_channel::<String>();
667 non_interactive::run_server(&state, llm, srv_rx, limits).await
668}
669
670#[cfg(not(target_arch = "wasm32"))]
672async fn run_simple_protocol(protocol: &str, args: &Args) -> Result<()> {
673 use crate::protocol::EASY_REGISTRY;
674 use std::sync::Arc;
675 use std::time::Duration;
676 use tokio::sync::Mutex;
677
678 setup::init_logging(args, false)?;
680
681 if EASY_REGISTRY.get_by_name(protocol).is_none() {
683 eprintln!("Error: Unknown simple protocol: {}", protocol);
684 eprintln!("Use --simple-list to see available protocols");
685 std::process::exit(1);
686 }
687
688 println!("[SIMPLE] Starting simple protocol: {}", protocol);
689
690 let settings = Settings::load();
692
693 let base_url = args
695 .openai_url
696 .clone()
697 .or_else(|| args.ollama_url.clone())
698 .unwrap_or_else(|| "http://localhost:11434".to_string());
699 let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
700 state.set_min_stability(args.parse_min_stability()?).await;
701
702 let rate_limiter_config = args.build_rate_limiter_config();
704 state.configure_rate_limiter(rate_limiter_config).await?;
705
706 let configured_model = args.model.clone().or(settings.model.clone());
708
709 let selected_model = if args.openai_url.is_some() {
711 configured_model
712 .ok_or_else(|| anyhow::anyhow!("--model is required when using --openai-url"))?
713 } else {
714 let ollama_url_for_model = args
715 .ollama_url
716 .as_deref()
717 .unwrap_or("http://localhost:11434");
718 crate::llm::select_or_validate_model(configured_model, false, ollama_url_for_model)
719 .await?
720 .ok_or_else(|| anyhow::anyhow!("No model available"))?
721 };
722
723 println!("[SIMPLE] Using model: {}", selected_model);
724 state.set_ollama_model(Some(selected_model)).await;
725
726 let llm = create_llm_client(args)?
728 .with_mock_config_file(args.mock_config_file.clone())
729 .with_app_state(state.clone());
730
731 state.set_llm_client(llm.clone()).await;
733
734 let easy_id = easy_startup::start_easy_protocol(
736 protocol,
737 None, None, Arc::new(state.clone()),
740 Arc::new(llm.clone()),
741 )
742 .await?;
743
744 println!(
745 "[SIMPLE] Started {} (easy instance #{})",
746 protocol,
747 easy_id.as_u32()
748 );
749
750 if let Some(server_id) = state.get_first_server_id().await {
752 if let Some(server) = state.get_server(server_id).await {
753 println!("[SIMPLE] Listening on port {}", server.port);
754 }
755 println!(
756 "[SIMPLE] Server #{} is running. Press Ctrl+C to stop.",
757 server_id.as_u32()
758 );
759 }
760
761 let shutdown = Arc::new(Mutex::new(false));
763 let shutdown_clone = shutdown.clone();
764 tokio::spawn(async move {
765 tokio::signal::ctrl_c().await.ok();
766 let mut shutdown = shutdown_clone.lock().await;
767 *shutdown = true;
768 });
769
770 loop {
772 tokio::time::sleep(Duration::from_millis(100)).await;
773 if *shutdown.lock().await {
774 println!("\n[SIMPLE] Shutting down...");
775 break;
776 }
777 }
778
779 println!("[SIMPLE] Server stopped.");
780 Ok(())
781}