1use anyhow::Result;
4use tokio::sync::mpsc;
5use tracing::info;
6
7use super::errors::ActionExecutionError;
8use super::types::{AppEvent, UserCommand};
9use crate::cli::server_startup;
10use crate::llm::actions::{get_all_tool_actions, get_user_input_common_actions};
11use crate::llm::format_indented_dimmed_lines;
12use crate::llm::OllamaClient;
13use crate::llm::{execute_actions, CommonAction, Server};
14use crate::state::app_state::{AppState, Mode};
15use crate::ui::App;
16
17const REQUIRE_DOCS_FOR_OPEN_ACTIONS: bool = false;
48
49fn log_open_server_summary(
51 status_tx: &mpsc::UnboundedSender<String>,
52 mac_address: &Option<String>,
53 interface: &Option<String>,
54 host: &Option<String>,
55 port: Option<u16>,
56 protocol: &str,
57 send_first: bool,
58 initial_memory: &Option<String>,
59 instruction: &str,
60 startup_params: &Option<serde_json::Value>,
61 event_handlers: &Option<Vec<serde_json::Value>>,
62 scheduled_tasks: &Option<Vec<crate::llm::actions::common::ServerTaskDefinition>>,
63 feedback_instructions: &Option<String>,
64) {
65 let _ = status_tx.send("[INFO] ═══ open_server ═══".to_string());
67
68 let _ = status_tx.send(format!("[INFO] Protocol: {}", protocol));
70 if let Some(p) = port {
71 let _ = status_tx.send(format!("[INFO] Port: {}", p));
72 }
73 if let Some(h) = host {
74 let _ = status_tx.send(format!("[INFO] Host: {}", h));
75 }
76 if let Some(i) = interface {
77 let _ = status_tx.send(format!("[INFO] Interface: {}", i));
78 }
79 if let Some(m) = mac_address {
80 let _ = status_tx.send(format!("[INFO] MAC Address: {}", m));
81 }
82 if send_first {
83 let _ = status_tx.send("[INFO] Send First: yes".to_string());
84 }
85
86 if !instruction.is_empty() {
88 let truncated = crate::utils::truncate_for_log(instruction, 100);
89 let _ = status_tx.send(format!("[INFO] Instruction: {}", truncated));
90 }
91
92 if let Some(mem) = initial_memory {
94 if !mem.is_empty() {
95 let truncated = crate::utils::truncate_for_log(mem, 100);
96 let _ = status_tx.send(format!("[INFO] Initial Memory: {}", truncated));
97 }
98 }
99
100 if let Some(params) = startup_params {
102 if !params.is_null() {
103 let shown = crate::utils::redact::redact_sensitive(params);
104 let params_str =
105 serde_json::to_string_pretty(&shown).unwrap_or_else(|_| shown.to_string());
106 let _ = status_tx.send("[INFO] Startup Params:".to_string());
107 for line in format_indented_dimmed_lines(¶ms_str, 8) {
108 let _ = status_tx.send(format!("[INFO] {}", line));
109 }
110 }
111 }
112
113 if let Some(handlers) = event_handlers {
115 let _ = status_tx.send(format!(
116 "[INFO] Event Handlers: {} configured",
117 handlers.len()
118 ));
119 for handler in handlers {
120 let event_pattern = handler
121 .get("event_pattern")
122 .and_then(|v| v.as_str())
123 .unwrap_or("*");
124 let handler_type = handler
125 .get("handler")
126 .and_then(|h| h.get("type"))
127 .and_then(|t| t.as_str())
128 .unwrap_or("unknown");
129
130 let _ = status_tx.send(format!("[INFO] • {} → {}", event_pattern, handler_type));
131
132 if handler_type == "script" {
134 if let Some(code) = handler
135 .get("handler")
136 .and_then(|h| h.get("code"))
137 .and_then(|c| c.as_str())
138 {
139 let truncated = crate::utils::truncate_for_log(code, 200);
140 for line in format_indented_dimmed_lines(&truncated, 8) {
141 let _ = status_tx.send(format!("[INFO] {}", line));
142 }
143 }
144 }
145
146 if handler_type == "static" {
148 if let Some(actions) = handler.get("handler").and_then(|h| h.get("actions")) {
149 let actions_str = serde_json::to_string_pretty(actions)
150 .unwrap_or_else(|_| actions.to_string());
151 let truncated = crate::utils::truncate_for_log(&actions_str, 300);
152 for line in format_indented_dimmed_lines(&truncated, 8) {
153 let _ = status_tx.send(format!("[INFO] {}", line));
154 }
155 }
156 }
157 }
158 }
159
160 if let Some(tasks) = scheduled_tasks {
162 let _ = status_tx.send(format!(
163 "[INFO] Scheduled Tasks: {} configured",
164 tasks.len()
165 ));
166 for task in tasks {
167 let timing = if task.recurring {
168 task.interval_secs
169 .map(|i| format!("every {}s", i))
170 .unwrap_or_else(|| "recurring".to_string())
171 } else {
172 task.delay_secs
173 .map(|d| format!("after {}s", d))
174 .unwrap_or_else(|| "one-shot".to_string())
175 };
176 let _ = status_tx.send(format!("[INFO] • {} ({})", task.task_id, timing));
177 let truncated = crate::utils::truncate_for_log(&task.instruction, 100);
179 for line in format_indented_dimmed_lines(&truncated, 8) {
180 let _ = status_tx.send(format!("[INFO] {}", line));
181 }
182 }
183 }
184
185 if let Some(fb) = feedback_instructions {
187 let truncated = crate::utils::truncate_for_log(fb, 100);
188 let _ = status_tx.send(format!("[INFO] Feedback Instructions: {}", truncated));
189 }
190
191 let _ = status_tx.send("[INFO] ══════════════════".to_string());
192}
193
194fn log_open_client_summary(
196 status_tx: &mpsc::UnboundedSender<String>,
197 protocol: &str,
198 remote_addr: &str,
199 instruction: &str,
200 startup_params: &Option<serde_json::Value>,
201 initial_memory: &Option<String>,
202 event_handlers: &Option<Vec<serde_json::Value>>,
203 scheduled_tasks: &Option<Vec<crate::llm::actions::common::ServerTaskDefinition>>,
204 feedback_instructions: &Option<String>,
205) {
206 let _ = status_tx.send("[INFO] ═══ open_client ═══".to_string());
208
209 let _ = status_tx.send(format!("[INFO] Protocol: {}", protocol));
211 let _ = status_tx.send(format!("[INFO] Remote: {}", remote_addr));
212
213 if !instruction.is_empty() {
215 let truncated = crate::utils::truncate_for_log(instruction, 100);
216 let _ = status_tx.send(format!("[INFO] Instruction: {}", truncated));
217 }
218
219 if let Some(mem) = initial_memory {
221 if !mem.is_empty() {
222 let truncated = crate::utils::truncate_for_log(mem, 100);
223 let _ = status_tx.send(format!("[INFO] Initial Memory: {}", truncated));
224 }
225 }
226
227 if let Some(params) = startup_params {
229 if !params.is_null() {
230 let shown = crate::utils::redact::redact_sensitive(params);
231 let params_str =
232 serde_json::to_string_pretty(&shown).unwrap_or_else(|_| shown.to_string());
233 let _ = status_tx.send("[INFO] Startup Params:".to_string());
234 for line in format_indented_dimmed_lines(¶ms_str, 8) {
235 let _ = status_tx.send(format!("[INFO] {}", line));
236 }
237 }
238 }
239
240 if let Some(handlers) = event_handlers {
242 let _ = status_tx.send(format!(
243 "[INFO] Event Handlers: {} configured",
244 handlers.len()
245 ));
246 for handler in handlers {
247 let event_pattern = handler
248 .get("event_pattern")
249 .and_then(|v| v.as_str())
250 .unwrap_or("*");
251 let handler_type = handler
252 .get("handler")
253 .and_then(|h| h.get("type"))
254 .and_then(|t| t.as_str())
255 .unwrap_or("unknown");
256
257 let _ = status_tx.send(format!("[INFO] • {} → {}", event_pattern, handler_type));
258
259 if handler_type == "script" {
260 if let Some(code) = handler
261 .get("handler")
262 .and_then(|h| h.get("code"))
263 .and_then(|c| c.as_str())
264 {
265 let truncated = crate::utils::truncate_for_log(code, 200);
266 for line in format_indented_dimmed_lines(&truncated, 8) {
267 let _ = status_tx.send(format!("[INFO] {}", line));
268 }
269 }
270 }
271
272 if handler_type == "static" {
273 if let Some(actions) = handler.get("handler").and_then(|h| h.get("actions")) {
274 let actions_str = serde_json::to_string_pretty(actions)
275 .unwrap_or_else(|_| actions.to_string());
276 let truncated = crate::utils::truncate_for_log(&actions_str, 300);
277 for line in format_indented_dimmed_lines(&truncated, 8) {
278 let _ = status_tx.send(format!("[INFO] {}", line));
279 }
280 }
281 }
282 }
283 }
284
285 if let Some(tasks) = scheduled_tasks {
287 let _ = status_tx.send(format!(
288 "[INFO] Scheduled Tasks: {} configured",
289 tasks.len()
290 ));
291 for task in tasks {
292 let timing = if task.recurring {
293 task.interval_secs
294 .map(|i| format!("every {}s", i))
295 .unwrap_or_else(|| "recurring".to_string())
296 } else {
297 task.delay_secs
298 .map(|d| format!("after {}s", d))
299 .unwrap_or_else(|| "one-shot".to_string())
300 };
301 let _ = status_tx.send(format!("[INFO] • {} ({})", task.task_id, timing));
302 let truncated = crate::utils::truncate_for_log(&task.instruction, 100);
303 for line in format_indented_dimmed_lines(&truncated, 8) {
304 let _ = status_tx.send(format!("[INFO] {}", line));
305 }
306 }
307 }
308
309 if let Some(fb) = feedback_instructions {
311 let truncated = crate::utils::truncate_for_log(fb, 100);
312 let _ = status_tx.send(format!("[INFO] Feedback Instructions: {}", truncated));
313 }
314
315 let _ = status_tx.send("[INFO] ══════════════════".to_string());
316}
317
318#[derive(Clone)]
320pub struct EventHandler {
321 state: AppState,
323 llm: OllamaClient,
325}
326
327impl EventHandler {
328 pub fn new(state: AppState, llm: OllamaClient) -> Self {
330 Self { state, llm }
331 }
332
333 pub async fn list_models(&self) -> Result<Vec<String>> {
335 self.llm.list_models().await
336 }
337
338 pub fn get_llm_client(&self) -> OllamaClient {
340 self.llm.clone()
341 }
342
343 pub fn set_llm_client(&mut self, client: OllamaClient) {
345 self.llm = client;
346 }
347
348 pub async fn handle_event(&mut self, event: AppEvent, ui: &mut App) -> Result<bool> {
351 match event {
352 AppEvent::UserCommand(cmd) => self.handle_user_command(cmd, ui).await,
353 AppEvent::Tick => {
354 Ok(false)
356 }
357 AppEvent::Shutdown => {
358 info!("Shutdown event received");
359 Ok(true)
360 }
361 }
362 }
363
364 async fn handle_user_command(&mut self, command: UserCommand, ui: &mut App) -> Result<bool> {
367 match command {
368 UserCommand::Status => {
369 self.handle_status(ui).await?;
370 Ok(false)
371 }
372 UserCommand::ShowModel => {
373 self.handle_show_model(ui).await?;
374 Ok(false)
375 }
376 UserCommand::ChangeModel { model } => {
377 self.handle_change_model(model, ui).await?;
378 Ok(false)
379 }
380 UserCommand::ShowBackend => {
381 self.handle_show_backend(ui).await?;
382 Ok(false)
383 }
384 UserCommand::SetBackend { args } => {
385 self.handle_set_backend(args, ui).await?;
386 Ok(false)
387 }
388 UserCommand::ShowLogLevel => {
389 ui.add_llm_message(format!("Current log level: {}", ui.log_level.as_str()));
390 Ok(false)
391 }
392 UserCommand::ChangeLogLevel { level } => {
393 use crate::ui::app::LogLevel;
394 if let Some(log_level) = LogLevel::parse(&level) {
395 ui.set_log_level(log_level);
396 } else {
397 ui.add_llm_message(format!(
398 "Invalid log level: {level}. Use: error, warn, info, debug, or trace"
399 ));
400 }
401 Ok(false)
402 }
403 UserCommand::TestOutput { count } => {
404 for i in 0..count {
406 ui.add_llm_message(format!("Test line {} of {}", i + 1, count));
407 }
408 Ok(false)
409 }
410 UserCommand::TestAsk => {
411 use crate::llm::actions::tools::{execute_tool, ToolAction};
413
414 ui.add_llm_message(
415 "[INFO] Testing web search approval with DuckDuckGo...".to_string(),
416 );
417
418 let web_search_mode = self.state.get_web_search_mode().await;
420 let approval_tx = self.state.get_web_approval_channel().await;
421
422 let action = ToolAction::WebSearch {
424 query: "https://duckduckgo.com/?q=test+search+query+with+very+long+parameters&ia=web&category=general&filters=none".to_string(),
425 };
426
427 let result = execute_tool(
429 &action,
430 approval_tx.as_ref(),
431 web_search_mode,
432 Some(&self.state),
433 )
434 .await;
435
436 if result.success {
438 ui.add_llm_message("[INFO] Web search completed successfully".to_string());
439 ui.add_llm_message(format!("[DEBUG] Result: {}", result.result));
440 } else {
441 ui.add_llm_message(format!("[ERROR] Web search failed: {}", result.result));
442 }
443
444 Ok(false)
445 }
446 UserCommand::SetFooterStatus { message } => {
447 if message.is_some() {
449 ui.add_llm_message(
450 "Footer status command is only supported in rolling TUI mode".to_string(),
451 );
452 }
453 Ok(false)
454 }
455 UserCommand::StopAll => {
456 self.handle_stop_all(ui).await?;
457 Ok(false)
458 }
459 UserCommand::StopById { id } => {
460 self.handle_stop_by_id(id, ui).await?;
461 Ok(false)
462 }
463 UserCommand::Save { name, id } => {
464 self.handle_save(name, id, ui).await?;
465 Ok(false)
466 }
467 UserCommand::Load { name } => {
468 self.handle_load(name, ui).await?;
469 Ok(false)
470 }
471 #[cfg(feature = "sqlite")]
472 UserCommand::Sqlite { db_id, query } => {
473 self.handle_sqlite(db_id, query, ui).await?;
474 Ok(false)
475 }
476 UserCommand::Quit => {
477 self.handle_quit(ui).await?;
478 Ok(true) }
480 UserCommand::UnknownSlashCommand { command } => {
481 ui.add_llm_message(format!("Unknown command: {command}"));
482 ui.add_llm_message(
483 "Available commands: /status, /model [name], /log [level], /docs [protocol], /quit".to_string(),
484 );
485 Ok(false)
486 }
487 UserCommand::Interpret { input: _ } => {
488 ui.add_llm_message(
489 "Internal error: Interpret command should use async path".to_string(),
490 );
491 Ok(false)
492 }
493 UserCommand::ShowWebSearch => {
494 ui.add_llm_message(
496 "Web search command is only supported in rolling TUI mode".to_string(),
497 );
498 Ok(false)
499 }
500 UserCommand::SetWebSearch { mode: _ } => {
501 ui.add_llm_message(
503 "Web search command is only supported in rolling TUI mode".to_string(),
504 );
505 Ok(false)
506 }
507 UserCommand::ShowEventHandler => {
508 ui.add_llm_message(
510 "Event handler command is only supported in rolling TUI mode".to_string(),
511 );
512 Ok(false)
513 }
514 UserCommand::SetEventHandler { mode: _ } => {
515 ui.add_llm_message(
517 "Event handler command is only supported in rolling TUI mode".to_string(),
518 );
519 Ok(false)
520 }
521 UserCommand::ShowDocs { protocol } => {
522 self.handle_show_docs(protocol, ui).await?;
523 Ok(false)
524 }
525 UserCommand::ShowEnvironment => {
526 self.handle_show_environment(ui).await?;
527 Ok(false)
528 }
529 UserCommand::ShowUsage => {
530 ui.add_llm_message(
532 "Usage command is only supported in rolling TUI mode".to_string(),
533 );
534 Ok(false)
535 }
536 UserCommand::ShowStability => {
537 let min = self.state.get_min_stability().await;
538 for line in crate::protocol::stability_report(min) {
539 ui.add_llm_message(line);
540 }
541 Ok(false)
542 }
543 UserCommand::ListSimple => {
544 ui.add_llm_message(
546 "Simple protocol command is only supported in rolling TUI mode".to_string(),
547 );
548 Ok(false)
549 }
550 UserCommand::StartSimple { protocol: _ } => {
551 ui.add_llm_message(
553 "Simple protocol command is only supported in rolling TUI mode".to_string(),
554 );
555 Ok(false)
556 }
557 UserCommand::Manage | UserCommand::Update { .. } => {
558 ui.add_llm_message(
560 "The /manage and /update commands are only supported in rolling TUI mode"
561 .to_string(),
562 );
563 Ok(false)
564 }
565 }
566 }
567
568 pub async fn handle_interpret_with_actions(
571 &mut self,
572 input: String,
573 status_tx: mpsc::UnboundedSender<String>,
574 protocol: Option<Box<dyn Server + Send>>,
575 ) -> Result<()> {
576 use crate::llm::{ConversationHandler, PromptBuilder};
577
578 let protocol_async_actions = if let Some(ref proto) = protocol {
580 proto.get_async_actions(&self.state)
581 } else {
582 Vec::new()
583 };
584
585 let current_model = self.state.get_ollama_model().await;
587 let model = match crate::llm::ensure_model_selected(
588 current_model.clone(),
589 &self.state.get_ollama_url().await,
590 )
591 .await
592 {
593 Ok(m) => m,
594 Err(e) => {
595 return Err(anyhow::anyhow!("Failed to ensure model is selected: {}", e));
596 }
597 };
598
599 if current_model.is_none() {
601 let _ = status_tx.send(format!(
602 "⚠ Auto-selected model: {} (no model was configured)",
603 model
604 ));
605 }
606
607 let llm_with_status = self.llm.clone().with_status_tx(status_tx.clone());
609
610 let web_search_mode = self.state.get_web_search_mode().await;
612 let approval_tx = self.state.get_web_approval_channel().await;
613
614 let conversation_history = self.state.get_user_conversation_history().await;
616
617 let is_open_server_enabled = if REQUIRE_DOCS_FOR_OPEN_ACTIONS {
619 self.state.is_server_docs_read().await
620 } else {
621 true
622 };
623 let is_open_client_enabled = if REQUIRE_DOCS_FOR_OPEN_ACTIONS {
624 self.state.is_client_docs_read().await
625 } else {
626 true
627 };
628
629 let system_prompt = PromptBuilder::build_user_input_system_prompt_with_docs(
631 &self.state,
632 protocol_async_actions.clone(),
633 conversation_history,
634 is_open_server_enabled,
635 is_open_client_enabled,
636 )
637 .await;
638
639 let selected_mode = self.state.get_selected_scripting_mode().await;
641 let scripting_env = self.state.get_scripting_env().await;
642 let mut available_actions = get_user_input_common_actions(
643 selected_mode,
644 &scripting_env,
645 is_open_server_enabled,
646 is_open_client_enabled,
647 );
648 available_actions.extend(get_all_tool_actions(web_search_mode));
649 available_actions.extend(protocol_async_actions);
650
651 let available_actions =
657 PromptBuilder::advertised_user_input_actions(&self.state, available_actions).await;
658
659 let conversation_state = self.state.get_or_create_user_conversation_state().await;
661
662 let rate_limiter = self.state.get_rate_limiter().await;
664
665 let mut conversation = ConversationHandler::new(
666 system_prompt,
667 std::sync::Arc::new(llm_with_status),
668 model,
669 rate_limiter,
670 crate::llm::RequestSource::User, )
672 .with_native_tools(&available_actions)
673 .with_status_tx(status_tx.clone())
674 .with_tracking(
675 self.state.clone(),
676 crate::state::app_state::ConversationSource::User,
677 input.clone(),
678 )
679 .with_conversation_state(conversation_state);
680
681 self.state
683 .register_conversation(
684 conversation.conversation_id().to_string(),
685 crate::state::app_state::ConversationSource::User,
686 input.clone(),
687 )
688 .await;
689
690 conversation.mark_registered();
692
693 conversation.add_user_message(input);
695
696 const MAX_EXECUTION_RETRIES: usize = 1;
698 let mut execution_attempts = 0;
699
700 loop {
701 let actions = conversation
703 .generate_with_tools_and_retry(
704 approval_tx.clone(),
705 web_search_mode,
706 available_actions.clone(),
707 )
708 .await;
709
710 match actions {
711 Ok(action_values) => {
712 let mut should_retry = false;
713 let mut retry_error: Option<crate::events::ActionExecutionError> = None;
714
715 let mut state_changed = false;
717 for action_value in &action_values {
718 if let Ok(common_action) = CommonAction::from_json(action_value) {
719 let modifies_state = matches!(
721 common_action,
722 CommonAction::OpenServer { .. }
723 | CommonAction::CloseServer { .. }
724 | CommonAction::CloseAllServers
725 | CommonAction::OpenClient { .. }
726 | CommonAction::CloseClient { .. }
727 | CommonAction::CloseAllClients
728 | CommonAction::CloseConnectionById { .. }
729 | CommonAction::UpdateServer { .. }
730 | CommonAction::UpdateClient { .. }
731 );
732
733 match self
734 .execute_server_management_action(common_action, &status_tx)
735 .await
736 {
737 Ok(_) => {
738 if modifies_state {
740 state_changed = true;
741 }
742 }
743 Err(e)
744 if e.is_retryable()
745 && execution_attempts < MAX_EXECUTION_RETRIES =>
746 {
747 should_retry = true;
749 retry_error = Some(e);
750 break; }
752 Err(e) => {
753 let _ = status_tx
755 .send(format!("[ERROR] Error executing action: {e}"));
756 }
757 }
758 }
759 }
760
761 if state_changed && !should_retry {
763 conversation.update_current_state(&self.state, None).await;
764 let _ = status_tx.send(
765 "[DEBUG] Updated conversation state after server changes".to_string(),
766 );
767 }
768
769 if should_retry {
771 if let Some(error) = retry_error {
772 execution_attempts += 1;
773 let _ = status_tx.send(format!(
774 "[INFO] Execution error (attempt {}/{}), retrying with LLM feedback...",
775 execution_attempts,
776 MAX_EXECUTION_RETRIES + 1
777 ));
778
779 let correction = error.build_correction_message();
781 conversation.add_user_message(correction);
782
783 continue;
785 }
786 }
787
788 let protocol_ref: Option<&dyn Server> =
790 protocol.as_ref().map(|p| p.as_ref() as &dyn Server);
791
792 match execute_actions(
794 action_values.clone(),
795 &self.state,
796 protocol_ref,
797 None,
798 None,
799 )
800 .await
801 {
802 Ok(result) => {
803 for msg in result.messages {
805 let _ = status_tx.send(msg);
806 }
807 }
808 Err(e) => {
809 let _ =
810 status_tx.send(format!("[ERROR] Failed to execute actions: {e}"));
811 }
812 }
813
814 break;
816 }
817 Err(e) => {
818 let _ = status_tx.send(format!("[ERROR] LLM error: {e}"));
819 conversation.end_tracking().await;
821 break; }
823 }
824 }
825
826 Ok(())
827 }
828
829 pub async fn execute_server_management_action(
836 &mut self,
837 action: CommonAction,
838 status_tx: &mpsc::UnboundedSender<String>,
839 ) -> Result<(), crate::events::ActionExecutionError> {
840 match action {
841 CommonAction::OpenServer {
842 mac_address,
843 interface,
844 host,
845 port,
846 protocol,
847 send_first,
848 initial_memory,
849 instruction,
850 startup_params,
851 event_handlers,
852 scheduled_tasks,
853 feedback_instructions,
854 } => {
855 if REQUIRE_DOCS_FOR_OPEN_ACTIONS && !self.state.is_server_docs_read().await {
860 use crate::llm::actions::tools::{execute_tool, ToolAction};
862 let doc_tool = ToolAction::ReadDocumentation {
863 protocols: vec![protocol.clone()],
864 protocol: None,
865 };
866
867 let approval_tx = self.state.get_web_approval_channel().await;
869 let web_search_mode = self.state.get_web_search_mode().await;
870 let tool_result = execute_tool(
871 &doc_tool,
872 approval_tx.as_ref(),
873 web_search_mode,
874 Some(&self.state),
875 )
876 .await;
877
878 if tool_result.success {
879 self.state
881 .mark_server_protocols_documented(&[protocol.clone()])
882 .await;
883
884 let original_action = serde_json::json!({
886 "type": "open_server",
887 "mac_address": mac_address,
888 "interface": interface,
889 "host": host,
890 "port": port,
891 "protocol": protocol,
892 "send_first": send_first,
893 "initial_memory": initial_memory,
894 "instruction": instruction,
895 "startup_params": startup_params,
896 "event_handlers": event_handlers,
897 "scheduled_tasks": scheduled_tasks,
898 "feedback_instructions": feedback_instructions,
899 });
900
901 return Err(ActionExecutionError::DocumentationRequired {
903 action_type: "open_server".to_string(),
904 protocol: protocol.clone(),
905 documentation: tool_result.result,
906 original_action,
907 });
908 } else {
909 let _ = status_tx.send(format!(
911 "[WARN] Failed to fetch documentation for {}: {}. Proceeding anyway.",
912 protocol, tool_result.result
913 ));
914 }
915 }
916
917 log_open_server_summary(
919 status_tx,
920 &mac_address,
921 &interface,
922 &host,
923 port,
924 &protocol,
925 send_first,
926 &initial_memory,
927 &instruction,
928 &startup_params,
929 &event_handlers,
930 &scheduled_tasks,
931 &feedback_instructions,
932 );
933
934 let mac_address_clone = mac_address.clone();
936 let interface_clone = interface.clone();
937 let host_clone = host.clone();
938 let initial_memory_clone = initial_memory.clone();
939 let instruction_clone = instruction.clone();
940 let startup_params_clone = startup_params.clone();
941 let event_handlers_clone = event_handlers.clone();
942 let scheduled_tasks_clone = scheduled_tasks.clone();
943 let feedback_instructions_clone = feedback_instructions.clone();
944
945 match server_startup::start_server_from_action(
948 &self.state,
949 mac_address,
950 interface,
951 host,
952 port,
953 &protocol,
954 send_first,
955 initial_memory,
956 instruction,
957 startup_params,
958 event_handlers,
959 scheduled_tasks,
960 feedback_instructions,
961 status_tx.clone(),
962 )
963 .await
964 {
965 Ok(server_id) => {
966 let _ = status_tx.send(format!(
968 "[INFO] Server #{} started successfully",
969 server_id.as_u32()
970 ));
971 }
972 Err(e) => {
973 let error_msg = e.to_string();
975
976 let is_port_conflict = error_msg.contains("Address already in use")
978 || error_msg.contains("port conflict")
979 || error_msg.contains("bind");
980
981 let is_validation_error = error_msg
983 .contains("Invalid event handler configuration")
984 || error_msg.contains("Missing 'instruction' field")
985 || error_msg.contains("instruction is required");
986
987 if is_port_conflict && port.is_some() {
988 return Err(ActionExecutionError::PortConflict {
990 port: port.unwrap(),
991 protocol: protocol.to_string(),
992 underlying_error: error_msg,
993 });
994 } else if is_validation_error {
995 let original_action = serde_json::json!({
997 "type": "open_server",
998 "mac_address": mac_address_clone,
999 "interface": interface_clone,
1000 "host": host_clone,
1001 "port": port,
1002 "protocol": protocol,
1003 "send_first": send_first,
1004 "initial_memory": initial_memory_clone,
1005 "instruction": instruction_clone,
1006 "startup_params": startup_params_clone,
1007 "event_handlers": event_handlers_clone,
1008 "scheduled_tasks": scheduled_tasks_clone,
1009 "feedback_instructions": feedback_instructions_clone,
1010 });
1011
1012 let parameter_name = event_handler_parameter_name(&error_msg);
1013
1014 return Err(ActionExecutionError::InvalidActionParameters {
1015 action_type: "open_server".to_string(),
1016 parameter_name: parameter_name.to_string(),
1017 error_message: error_msg.clone(),
1018 original_action,
1019 });
1020 } else {
1021 let _ = status_tx
1023 .send(format!("[ERROR] Failed to start server: {}", error_msg));
1024 return Err(ActionExecutionError::Fatal(e));
1025 }
1026 }
1027 }
1028
1029 }
1031 CommonAction::CloseServer { server_id } => {
1032 use crate::state::server::ServerStatus;
1033
1034 let sid = crate::state::ServerId::new(server_id);
1036
1037 self.state
1039 .update_server_status(sid, ServerStatus::Stopped)
1040 .await;
1041 let _ = status_tx.send(format!("[SERVER] Stopped server #{}", sid.as_u32()));
1042
1043 self.state.cleanup_server_tasks(sid).await;
1045 let _ = status_tx.send(format!(
1046 "[TASK] Cleaned up tasks for server #{}",
1047 sid.as_u32()
1048 ));
1049
1050 crate::scripting::ResidentScriptManager::shutdown_server(sid.as_u32()).await;
1052
1053 let all_stopped =
1055 self.state.get_all_servers().await.iter().all(|s| {
1056 matches!(s.status, ServerStatus::Stopped | ServerStatus::Error(_))
1057 });
1058
1059 if all_stopped {
1060 self.state.set_mode(Mode::Idle).await;
1061 }
1062
1063 let _ = status_tx.send("__UPDATE_UI__".to_string());
1064 }
1065 CommonAction::CloseAllServers => {
1066 use crate::state::server::ServerStatus;
1067
1068 let server_ids = self.state.get_all_server_ids().await;
1070
1071 for server_id in server_ids {
1072 self.state
1074 .update_server_status(server_id, ServerStatus::Stopped)
1075 .await;
1076 let _ =
1077 status_tx.send(format!("[SERVER] Stopped server #{}", server_id.as_u32()));
1078
1079 self.state.cleanup_server_tasks(server_id).await;
1081 let _ = status_tx.send(format!(
1082 "[TASK] Cleaned up tasks for server #{}",
1083 server_id.as_u32()
1084 ));
1085
1086 crate::scripting::ResidentScriptManager::shutdown_server(server_id.as_u32())
1088 .await;
1089 }
1090
1091 self.state.set_mode(Mode::Idle).await;
1093
1094 let _ = status_tx.send("__UPDATE_UI__".to_string());
1095 }
1096 CommonAction::UpdateInstruction { instruction } => {
1097 if let Some(server_id) = self.state.get_first_server_id().await {
1099 self.state
1100 .set_instruction(server_id, instruction.clone())
1101 .await;
1102 let _ = status_tx.send(format!(
1103 "[INFO] Server #{} instruction: {}",
1104 server_id.as_u32(),
1105 instruction
1106 ));
1107 } else {
1108 let _ =
1109 status_tx.send("[WARN] No server to update instruction for".to_string());
1110 }
1111 }
1112 CommonAction::ChangeModel { model } => {
1113 self.state.set_ollama_model(Some(model.clone())).await;
1114 let _ = status_tx.send(format!("Changed model to: {model}"));
1115
1116 let _ = status_tx.send("__UPDATE_UI__".to_string());
1118 }
1119 CommonAction::SetMemory { value } => {
1121 if let Some(server_id) = self.state.get_first_server_id().await {
1122 self.state.set_memory(server_id, value).await;
1123 }
1124 }
1125 CommonAction::AppendMemory { value } => {
1126 if let Some(server_id) = self.state.get_first_server_id().await {
1127 self.state.append_memory(server_id, value).await;
1128 }
1129 }
1130 CommonAction::AppendToLog { .. } => {
1131 }
1134 CommonAction::ScheduleTask {
1135 task_id,
1136 recurring,
1137 delay_secs,
1138 interval_secs,
1139 max_executions,
1140 server_id,
1141 connection_id,
1142 client_id,
1143 instruction,
1144 context,
1145 script_runtime,
1146 script_language: _,
1147 script_path: _,
1148 script_inline,
1149 script_handles,
1150 } => {
1151 use crate::state::task::{ScheduledTask, TaskScope};
1152
1153 let scope = if let Some(conn_id_str) = connection_id {
1155 if let Some(sid) = server_id {
1157 let server_id_obj = crate::state::ServerId::new(sid);
1158 match crate::server::connection::ConnectionId::from_string(&conn_id_str) {
1159 Some(cid) => {
1160 if let Some(server) = self.state.get_server(server_id_obj).await {
1162 if server.connections.contains_key(&cid) {
1163 TaskScope::Connection(server_id_obj, cid)
1164 } else {
1165 let _ = status_tx.send(format!(
1166 "[ERROR] Connection {} not found on server #{}",
1167 conn_id_str, sid
1168 ));
1169 return Ok(());
1170 }
1171 } else {
1172 let _ = status_tx.send(format!(
1173 "[ERROR] Server #{} not found for connection-scoped task",
1174 sid
1175 ));
1176 return Ok(());
1177 }
1178 }
1179 None => {
1180 let _ = status_tx.send(format!(
1181 "[ERROR] Invalid connection_id format: {}. Expected 'conn-123' or '123'",
1182 conn_id_str
1183 ));
1184 return Ok(());
1185 }
1186 }
1187 } else {
1188 let _ = status_tx.send(
1189 "[ERROR] connection_id requires server_id to be specified".to_string(),
1190 );
1191 return Ok(());
1192 }
1193 } else if let Some(sid) = server_id {
1194 TaskScope::Server(crate::state::ServerId::new(sid))
1195 } else if let Some(cid) = client_id {
1196 let client_id_obj = crate::state::ClientId::new(cid);
1197 if self.state.get_client(client_id_obj).await.is_none() {
1199 let _ = status_tx.send(format!(
1200 "[ERROR] Client #{} not found for client-scoped task",
1201 cid
1202 ));
1203 return Ok(());
1204 }
1205 TaskScope::Client(client_id_obj)
1206 } else {
1207 TaskScope::Global
1208 };
1209
1210 let delay = if recurring {
1212 delay_secs.or(interval_secs).unwrap_or(0)
1213 } else {
1214 delay_secs.unwrap_or(0)
1215 };
1216
1217 let mut task = if recurring {
1218 let interval = interval_secs.unwrap_or(delay);
1219 ScheduledTask::new_recurring(
1220 crate::state::TaskId::new(0), task_id.clone(),
1222 scope,
1223 interval,
1224 max_executions,
1225 instruction,
1226 context,
1227 )
1228 } else {
1229 ScheduledTask::new_one_shot(
1230 crate::state::TaskId::new(0), task_id.clone(),
1232 scope,
1233 delay,
1234 instruction,
1235 context,
1236 )
1237 }?;
1238
1239 task.next_execution = crate::state::task::checked_task_deadline(delay)?;
1240
1241 let task_id_num = self.state.add_task(task).await;
1242
1243 let _ = script_runtime; let _ = script_inline; let _ = script_handles; if recurring {
1250 let interval = interval_secs.unwrap_or(delay);
1251 let max_info = if let Some(max) = max_executions {
1252 format!(" (max {} executions)", max)
1253 } else {
1254 String::new()
1255 };
1256 let _ = status_tx.send(format!(
1257 "[TASK] Scheduled recurring task '{}' (ID: {}) to execute every {}s{}",
1258 task_id, task_id_num, interval, max_info
1259 ));
1260 } else {
1261 let _ = status_tx.send(format!(
1262 "[TASK] Scheduled one-shot task '{}' (ID: {}) to execute in {}s",
1263 task_id, task_id_num, delay
1264 ));
1265 }
1266 }
1267 CommonAction::CancelTask { task_id } => {
1268 if let Some(task) = self.state.get_task(&task_id).await {
1269 self.state.remove_task(task.id).await;
1270 let _ = status_tx.send(format!("[TASK] Cancelled task '{}'", task_id));
1271 } else {
1272 let _ = status_tx.send(format!("[WARN] Task '{}' not found", task_id));
1273 }
1274 }
1275 CommonAction::OpenClient {
1276 protocol,
1277 remote_addr,
1278 instruction,
1279 startup_params,
1280 initial_memory,
1281 event_handlers,
1282 scheduled_tasks,
1283 feedback_instructions,
1284 } => {
1285 if REQUIRE_DOCS_FOR_OPEN_ACTIONS && !self.state.is_client_docs_read().await {
1290 use crate::llm::actions::tools::{execute_tool, ToolAction};
1292 let doc_tool = ToolAction::ReadDocumentation {
1293 protocols: vec![protocol.clone()],
1294 protocol: None,
1295 };
1296
1297 let approval_tx = self.state.get_web_approval_channel().await;
1299 let web_search_mode = self.state.get_web_search_mode().await;
1300 let tool_result = execute_tool(
1301 &doc_tool,
1302 approval_tx.as_ref(),
1303 web_search_mode,
1304 Some(&self.state),
1305 )
1306 .await;
1307
1308 if tool_result.success {
1309 self.state
1311 .mark_client_protocols_documented(&[protocol.clone()])
1312 .await;
1313
1314 let original_action = serde_json::json!({
1316 "type": "open_client",
1317 "protocol": protocol,
1318 "remote_addr": remote_addr,
1319 "instruction": instruction,
1320 "startup_params": startup_params,
1321 "initial_memory": initial_memory,
1322 "event_handlers": event_handlers,
1323 "scheduled_tasks": scheduled_tasks,
1324 "feedback_instructions": feedback_instructions,
1325 });
1326
1327 return Err(ActionExecutionError::DocumentationRequired {
1329 action_type: "open_client".to_string(),
1330 protocol: protocol.clone(),
1331 documentation: tool_result.result,
1332 original_action,
1333 });
1334 } else {
1335 let _ = status_tx.send(format!(
1337 "[WARN] Failed to fetch documentation for {}: {}. Proceeding anyway.",
1338 protocol, tool_result.result
1339 ));
1340 }
1341 }
1342
1343 let prepared_tasks = crate::state::task::prepare_tasks(
1344 scheduled_tasks.as_deref(),
1345 crate::state::task::TaskScope::Global,
1346 )?;
1347
1348 log_open_client_summary(
1350 status_tx,
1351 &protocol,
1352 &remote_addr,
1353 &instruction,
1354 &startup_params,
1355 &initial_memory,
1356 &event_handlers,
1357 &scheduled_tasks,
1358 &feedback_instructions,
1359 );
1360
1361 use crate::state::client::{ClientInstance, ClientStatus};
1362
1363 let event_handlers_clone = event_handlers.clone();
1365 let feedback_instructions_clone = feedback_instructions.clone();
1366 let initial_memory_clone = initial_memory.clone();
1367
1368 let parsed_event_handlers = match event_handlers {
1377 Some(handlers_json) => match Self::parse_event_handlers(handlers_json) {
1378 Ok(config) => Some(config),
1379 Err(e) => {
1380 let original_action = serde_json::json!({
1382 "type": "open_client",
1383 "protocol": protocol,
1384 "remote_addr": remote_addr,
1385 "instruction": instruction,
1386 "startup_params": startup_params,
1387 "initial_memory": initial_memory_clone,
1388 "event_handlers": event_handlers_clone,
1389 "scheduled_tasks": scheduled_tasks,
1390 "feedback_instructions": feedback_instructions_clone,
1391 });
1392
1393 let error_msg = e.to_string();
1394
1395 let _ = status_tx.send(format!(
1397 "[ERROR] Invalid event handler configuration: {}",
1398 error_msg
1399 ));
1400 return Err(ActionExecutionError::InvalidActionParameters {
1401 action_type: "open_client".to_string(),
1402 parameter_name: event_handler_parameter_name(&error_msg)
1403 .to_string(),
1404 error_message: error_msg,
1405 original_action,
1406 });
1407 }
1408 },
1409 None => None,
1410 };
1411
1412 let mut client = ClientInstance::new(
1414 crate::state::ClientId::new(0),
1415 remote_addr.clone(),
1416 protocol.clone(),
1417 instruction.clone(),
1418 );
1419
1420 if let Some(mem) = initial_memory {
1422 client.memory = mem;
1423 }
1424 client.startup_params = startup_params.clone();
1425 client.feedback_instructions = feedback_instructions;
1426
1427 let client_id = self.state.add_client(client).await;
1429
1430 if let Some(config) = parsed_event_handlers {
1433 self.state
1434 .set_client_event_handler_config(client_id, Some(config))
1435 .await;
1436 let _ = status_tx
1437 .send("[INFO] Event handler configuration applied to client".to_string());
1438 }
1439
1440 let _ = status_tx.send(format!(
1441 "[CLIENT] Opening {} client #{} to {}...",
1442 protocol,
1443 client_id.as_u32(),
1444 remote_addr
1445 ));
1446
1447 let llm_client = self.llm.clone();
1449 let status_tx_clone = status_tx.clone();
1450 match crate::cli::client_startup::start_client_by_id(
1451 &self.state,
1452 client_id,
1453 &llm_client,
1454 &status_tx_clone,
1455 )
1456 .await
1457 {
1458 Ok(_) => {
1459 let _ = status_tx.send(format!(
1461 "[CLIENT] {} client #{} connected",
1462 protocol,
1463 client_id.as_u32()
1464 ));
1465
1466 for mut task in prepared_tasks {
1467 task.scope = crate::state::task::TaskScope::Client(client_id);
1468 let task_name = task.name.clone();
1469 let task_id_num = self.state.add_task(task).await;
1470 let _ = status_tx.send(format!(
1471 "[TASK] Created task '{}' (ID: {}) for client #{}",
1472 task_name,
1473 task_id_num,
1474 client_id.as_u32()
1475 ));
1476 }
1477 }
1478 Err(e) => {
1479 self.state
1481 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
1482 .await;
1483 let _ = status_tx.send(format!(
1484 "[ERROR] Failed to connect {} client #{}: {}",
1485 protocol,
1486 client_id.as_u32(),
1487 e
1488 ));
1489 return Err(e);
1490 }
1491 }
1492
1493 let _ = status_tx.send("__UPDATE_UI__".to_string());
1494 }
1495 CommonAction::CloseClient { client_id } => {
1496 use crate::state::client::ClientStatus;
1497
1498 let cid = crate::state::ClientId::new(client_id);
1499
1500 self.state
1502 .update_client_status(cid, ClientStatus::Disconnected)
1503 .await;
1504 let _ = status_tx.send(format!("[CLIENT] Closed client #{}", cid.as_u32()));
1505
1506 self.state.cleanup_client_tasks(cid).await;
1508 let _ = status_tx.send(format!(
1509 "[TASK] Cleaned up tasks for client #{}",
1510 cid.as_u32()
1511 ));
1512
1513 let _ = status_tx.send("__UPDATE_UI__".to_string());
1514 }
1515 CommonAction::CloseAllClients => {
1516 use crate::state::client::ClientStatus;
1517
1518 let client_ids = self.state.get_all_client_ids().await;
1519
1520 for client_id in client_ids {
1521 self.state
1523 .update_client_status(client_id, ClientStatus::Disconnected)
1524 .await;
1525 let _ =
1526 status_tx.send(format!("[CLIENT] Closed client #{}", client_id.as_u32()));
1527
1528 self.state.cleanup_client_tasks(client_id).await;
1530 let _ = status_tx.send(format!(
1531 "[TASK] Cleaned up tasks for client #{}",
1532 client_id.as_u32()
1533 ));
1534 }
1535
1536 let _ = status_tx.send("__UPDATE_UI__".to_string());
1537 }
1538 CommonAction::CloseConnectionById { connection_id } => {
1539 use crate::server::connection::ConnectionId;
1540
1541 let conn_id = ConnectionId::new(connection_id);
1542 let all_servers = self.state.get_all_servers().await;
1543
1544 let mut found = false;
1545 for server in all_servers {
1546 if server.connections.contains_key(&conn_id) {
1547 self.state
1548 .close_connection_on_server(server.id, conn_id)
1549 .await;
1550 let _ = status_tx.send(format!(
1551 "[CONNECTION] Closed connection #{} on server #{}",
1552 connection_id,
1553 server.id.as_u32()
1554 ));
1555 found = true;
1556 break;
1557 }
1558 }
1559
1560 if !found {
1561 let _ =
1562 status_tx.send(format!("[ERROR] Connection #{} not found", connection_id));
1563 }
1564
1565 let _ = status_tx.send("__UPDATE_UI__".to_string());
1566 }
1567 CommonAction::ReconnectClient { client_id } => {
1568 let cid = crate::state::ClientId::new(client_id);
1569
1570 let llm_client = self.llm.clone();
1572 let status_tx_clone = status_tx.clone();
1573
1574 let _ =
1575 status_tx.send(format!("[CLIENT] Reconnecting client #{}...", cid.as_u32()));
1576
1577 match crate::cli::client_startup::start_client_by_id(
1578 &self.state,
1579 cid,
1580 &llm_client,
1581 &status_tx_clone,
1582 )
1583 .await
1584 {
1585 Ok(_) => {
1586 let _ = status_tx
1587 .send(format!("[CLIENT] Client #{} reconnected", cid.as_u32()));
1588 }
1589 Err(e) => {
1590 let _ = status_tx.send(format!(
1591 "[ERROR] Failed to reconnect client #{}: {}",
1592 cid.as_u32(),
1593 e
1594 ));
1595 return Err(e);
1596 }
1597 }
1598
1599 let _ = status_tx.send("__UPDATE_UI__".to_string());
1600 }
1601 CommonAction::UpdateClientInstruction {
1602 client_id,
1603 instruction,
1604 } => {
1605 let cid = crate::state::ClientId::new(client_id);
1606
1607 self.state
1609 .set_instruction_for_client(cid, instruction.clone())
1610 .await;
1611
1612 let _ = status_tx.send(format!(
1613 "[CLIENT] Updated instruction for client #{}",
1614 cid.as_u32()
1615 ));
1616 let _ = status_tx.send(format!("[CLIENT] New instruction: {}", instruction));
1617 let _ = status_tx.send("__UPDATE_UI__".to_string());
1618 }
1619 CommonAction::UpdateServer {
1620 server_id,
1621 port,
1622 host,
1623 interface,
1624 mac_address,
1625 send_first,
1626 instruction,
1627 initial_memory,
1628 startup_params,
1629 event_handlers,
1630 scheduled_tasks,
1631 feedback_instructions,
1632 } => {
1633 use crate::cli::management::{self, ServerForm};
1634 let form = ServerForm {
1635 protocol: String::new(), port,
1637 host,
1638 interface,
1639 mac_address,
1640 send_first,
1641 instruction,
1642 initial_memory,
1643 startup_params,
1644 event_handlers,
1645 scheduled_tasks,
1646 feedback_instructions,
1647 };
1648 match management::update_server(
1649 &self.state,
1650 crate::state::ServerId::new(server_id),
1651 form,
1652 status_tx.clone(),
1653 )
1654 .await
1655 {
1656 Ok(outcome) => {
1657 let _ = status_tx.send(format!("[INFO] {}", outcome.summary));
1658 }
1659 Err(e) => {
1660 let _ = status_tx.send(format!("[ERROR] Failed to update server: {}", e));
1661 return Err(crate::events::ActionExecutionError::Fatal(e));
1662 }
1663 }
1664 }
1665 CommonAction::UpdateClient {
1666 client_id,
1667 remote_addr,
1668 instruction,
1669 initial_memory,
1670 startup_params,
1671 event_handlers,
1672 scheduled_tasks,
1673 feedback_instructions,
1674 } => {
1675 use crate::cli::management::{self, ClientForm};
1676 let form = ClientForm {
1677 protocol: String::new(), remote_addr,
1679 instruction,
1680 initial_memory,
1681 startup_params,
1682 event_handlers,
1683 scheduled_tasks,
1684 feedback_instructions,
1685 };
1686 match management::update_client(
1687 &self.state,
1688 crate::state::ClientId::new(client_id),
1689 form,
1690 self.llm.clone(),
1691 status_tx.clone(),
1692 )
1693 .await
1694 {
1695 Ok(outcome) => {
1696 let _ = status_tx.send(format!("[INFO] {}", outcome.summary));
1697 }
1698 Err(e) => {
1699 let _ = status_tx.send(format!("[ERROR] Failed to update client: {}", e));
1700 return Err(crate::events::ActionExecutionError::Fatal(e));
1701 }
1702 }
1703 }
1704
1705 #[cfg(feature = "sqlite")]
1706 CommonAction::CreateDatabase {
1707 name,
1708 is_memory,
1709 owner,
1710 schema_ddl,
1711 } => {
1712 use crate::state::DatabaseOwner;
1713
1714 if let Err(e) = crate::state::sqlite::validate_database_name(&name) {
1720 let _ = status_tx.send(format!("[ERROR] {}", e));
1721 return Ok(());
1722 }
1723
1724 let db_path = if is_memory {
1726 crate::state::sqlite::MEMORY_DATABASE_PATH.to_string()
1727 } else {
1728 match crate::state::sqlite::database_file_path(&name) {
1729 Ok(p) => p,
1730 Err(e) => {
1731 let _ = status_tx.send(format!("[ERROR] {}", e));
1732 return Ok(());
1733 }
1734 }
1735 };
1736
1737 let db_owner = if let Some(owner_str) = owner {
1739 if owner_str == "global" {
1740 DatabaseOwner::Global
1741 } else if let Some(id_str) = owner_str.strip_prefix("server-") {
1742 if let Ok(id) = id_str.parse::<u32>() {
1743 DatabaseOwner::Server(crate::state::ServerId::new(id))
1744 } else {
1745 let _ = status_tx
1746 .send(format!("[ERROR] Invalid server ID in owner: {}", owner_str));
1747 return Ok(());
1748 }
1749 } else if let Some(id_str) = owner_str.strip_prefix("client-") {
1750 if let Ok(id) = id_str.parse::<u32>() {
1751 DatabaseOwner::Client(crate::state::ClientId::new(id))
1752 } else {
1753 let _ = status_tx
1754 .send(format!("[ERROR] Invalid client ID in owner: {}", owner_str));
1755 return Ok(());
1756 }
1757 } else {
1758 let _ =
1759 status_tx.send(format!("[ERROR] Invalid owner format: {}", owner_str));
1760 return Ok(());
1761 }
1762 } else {
1763 if let Some(sid) = self.state.get_first_server_id().await {
1765 DatabaseOwner::Server(sid)
1766 } else if let Some(cid) = self.state.get_first_client_id().await {
1767 DatabaseOwner::Client(cid)
1768 } else {
1769 DatabaseOwner::Global
1770 }
1771 };
1772
1773 match self
1775 .state
1776 .create_database(
1777 name.clone(),
1778 db_path.clone(),
1779 db_owner.clone(),
1780 schema_ddl.as_deref(),
1781 )
1782 .await
1783 {
1784 Ok(db_id) => {
1785 let _ = status_tx.send(format!(
1786 "[DB] Created database '{}' ({}) at {} (owner: {})",
1787 name, db_id, db_path, db_owner
1788 ));
1789
1790 if let Some(db) = self.state.get_database(db_id).await {
1792 if !db.tables.is_empty() {
1793 let _ =
1794 status_tx.send(format!("[DB] Schema: {}", db.schema_summary()));
1795 }
1796 }
1797 }
1798 Err(e) => {
1799 let _ = status_tx.send(format!("[ERROR] Failed to create database: {}", e));
1800 }
1801 }
1802 }
1803
1804 #[cfg(feature = "sqlite")]
1805 CommonAction::DeleteDatabase { database_id } => {
1806 let db_id = crate::state::DatabaseId::new(database_id);
1807
1808 match self.state.delete_database(db_id).await {
1809 Ok(_) => {
1810 let _ = status_tx.send(format!("[DB] Deleted database {}", db_id));
1811 }
1812 Err(e) => {
1813 let _ = status_tx.send(format!("[ERROR] Failed to delete database: {}", e));
1814 }
1815 }
1816 }
1817
1818 CommonAction::ShowMessage { message } => {
1819 let _ = status_tx.send(format!("[CLIENT] {}", message));
1820 }
1821 CommonAction::ProvideFeedback { .. } => {
1822 }
1825 }
1826
1827 Ok(())
1828 }
1829
1830 pub fn parse_event_handlers(
1844 handlers_json: Vec<serde_json::Value>,
1845 ) -> Result<crate::scripting::EventHandlerConfig> {
1846 use crate::scripting::{EventHandler, EventHandlerConfig, EventHandlerType, EventPattern};
1847
1848 let mut config = EventHandlerConfig::new();
1849
1850 for handler_json in handlers_json {
1851 let event_pattern = if let Some(pattern_str) =
1853 handler_json.get("event_pattern").and_then(|v| v.as_str())
1854 {
1855 EventPattern::from(pattern_str)
1856 } else {
1857 EventPattern::wildcard()
1859 };
1860
1861 let handler_type_json = handler_json.get("handler").ok_or_else(|| {
1863 anyhow::anyhow!("Missing 'handler' field in event handler configuration")
1864 })?;
1865
1866 let handler_type_str = handler_type_json
1867 .get("type")
1868 .and_then(|v| v.as_str())
1869 .ok_or_else(|| anyhow::anyhow!("Missing 'type' field in handler configuration"))?;
1870
1871 let handler_type = match handler_type_str {
1872 "llm" => {
1873 let instruction = handler_type_json
1874 .get("instruction")
1875 .and_then(|v| v.as_str())
1876 .ok_or_else(|| {
1877 anyhow::anyhow!("Missing 'instruction' field for llm handler - instruction is required for LLM event handlers")
1878 })?;
1879 EventHandlerType::llm(instruction)
1880 }
1881 "script" => {
1882 let language = handler_type_json
1883 .get("language")
1884 .and_then(|v| v.as_str())
1885 .ok_or_else(|| {
1886 anyhow::anyhow!("Missing 'language' field for script handler")
1887 })?;
1888 let code = handler_type_json
1889 .get("code")
1890 .and_then(|v| v.as_str())
1891 .ok_or_else(|| {
1892 anyhow::anyhow!("Missing 'code' field for script handler")
1893 })?;
1894 let resident = handler_type_json
1897 .get("resident")
1898 .and_then(|v| v.as_bool())
1899 .unwrap_or(false);
1900 if resident {
1901 let scope = handler_type_json
1902 .get("scope")
1903 .and_then(|v| v.as_str())
1904 .map(|s| s.to_string());
1905 EventHandlerType::script_resident(language, code, scope)
1906 } else {
1907 EventHandlerType::script(language, code)
1908 }
1909 }
1910 "static" => {
1911 let actions = handler_type_json
1912 .get("actions")
1913 .and_then(|v| v.as_array())
1914 .ok_or_else(|| {
1915 anyhow::anyhow!("Missing or invalid 'actions' field for static handler")
1916 })?;
1917 EventHandlerType::static_response(actions.clone())
1918 }
1919 "manual" => {
1922 let timeout_secs = match handler_type_json.get("timeout_secs") {
1923 None => crate::scripting::DEFAULT_MANUAL_TIMEOUT_SECS,
1924 Some(v) => v.as_u64().filter(|t| *t > 0).ok_or_else(|| {
1925 anyhow::anyhow!(
1926 "'timeout_secs' for a manual handler must be a positive integer \
1927 number of seconds"
1928 )
1929 })?,
1930 };
1931 EventHandlerType::manual(timeout_secs)
1932 }
1933 _ => anyhow::bail!("Unknown handler type: {}", handler_type_str),
1934 };
1935
1936 handler_type.validate().map_err(|e| {
1950 anyhow::anyhow!(
1951 "Invalid event handler for {}: {}",
1952 describe_pattern(&event_pattern),
1953 e
1954 )
1955 })?;
1956
1957 if let EventHandlerType::Static { ref actions } = handler_type {
1959 validate_static_action_names(&event_pattern, actions)?;
1960 }
1961
1962 config.add_handler(EventHandler::new(event_pattern, handler_type));
1963 }
1964
1965 Ok(config)
1966 }
1967
1968 async fn handle_quit(&mut self, ui: &mut App) -> Result<()> {
1969 ui.add_llm_message("Quitting...".to_string());
1970 Ok(())
1972 }
1973
1974 async fn handle_stop_all(&mut self, ui: &mut App) -> Result<()> {
1975 use crate::state::client::ClientStatus;
1976 use crate::state::server::ServerStatus;
1977
1978 ui.add_llm_message("Stopping all servers, connections, and clients...".to_string());
1979
1980 let server_ids: Vec<_> = self.state.get_all_server_ids().await;
1982 for server_id in server_ids {
1983 self.state
1984 .update_server_status(server_id, ServerStatus::Stopped)
1985 .await;
1986 self.state.cleanup_server_tasks(server_id).await;
1987 crate::scripting::ResidentScriptManager::shutdown_server(server_id.as_u32()).await;
1988 ui.add_llm_message(format!("[SERVER] Stopped server #{}", server_id.as_u32()));
1989 }
1990
1991 let client_ids: Vec<_> = self.state.get_all_client_ids().await;
1993 for client_id in client_ids {
1994 self.state
1995 .update_client_status(client_id, ClientStatus::Disconnected)
1996 .await;
1997 self.state.cleanup_client_tasks(client_id).await;
1998 ui.add_llm_message(format!("[CLIENT] Stopped client #{}", client_id.as_u32()));
1999 }
2000
2001 ui.add_llm_message("All servers and clients stopped.".to_string());
2002 Ok(())
2003 }
2004
2005 async fn handle_stop_by_id(&mut self, id: u32, ui: &mut App) -> Result<()> {
2006 use crate::server::connection::ConnectionId;
2007 use crate::state::client::{ClientId, ClientStatus};
2008 use crate::state::server::{ServerId, ServerStatus};
2009
2010 let mut found = false;
2012
2013 let server_id = ServerId::new(id);
2015 if self.state.get_server(server_id).await.is_some() {
2016 self.state
2017 .update_server_status(server_id, ServerStatus::Stopped)
2018 .await;
2019 self.state.cleanup_server_tasks(server_id).await;
2020 ui.add_llm_message(format!("[SERVER] Stopped server #{}", id));
2021 found = true;
2022 }
2023
2024 let client_id = ClientId::new(id);
2026 if self.state.get_client(client_id).await.is_some() {
2027 self.state
2028 .update_client_status(client_id, ClientStatus::Disconnected)
2029 .await;
2030 self.state.cleanup_client_tasks(client_id).await;
2031 ui.add_llm_message(format!("[CLIENT] Stopped client #{}", id));
2032 found = true;
2033 }
2034
2035 let connection_id = ConnectionId::new(id);
2037 let all_servers = self.state.get_all_servers().await;
2038 for server in all_servers {
2039 if server.connections.contains_key(&connection_id) {
2040 self.state
2041 .close_connection_on_server(server.id, connection_id)
2042 .await;
2043 ui.add_llm_message(format!(
2044 "[CONNECTION] Closed connection #{} on server #{}",
2045 id,
2046 server.id.as_u32()
2047 ));
2048 found = true;
2049 break;
2050 }
2051 }
2052
2053 if !found {
2054 ui.add_llm_message(format!(
2055 "No server, client, or connection found with ID #{}",
2056 id
2057 ));
2058 }
2059
2060 Ok(())
2061 }
2062
2063 async fn handle_status(&mut self, ui: &mut App) -> Result<()> {
2064 let summary = self.state.get_summary().await;
2065 ui.add_llm_message(format!("Status: {summary}"));
2066
2067 if let Some(server_id) = self.state.get_first_server_id().await {
2069 if let Some(instruction) = self.state.get_instruction(server_id).await {
2070 if !instruction.is_empty() {
2071 ui.add_llm_message(format!(
2072 "Server #{} instruction: {}",
2073 server_id.as_u32(),
2074 instruction
2075 ));
2076 }
2077 }
2078 } else {
2079 ui.add_llm_message("No servers running".to_string());
2080 }
2081
2082 Ok(())
2083 }
2084
2085 async fn handle_show_model(&mut self, ui: &mut App) -> Result<()> {
2086 let current_model = self
2087 .state
2088 .get_ollama_model()
2089 .await
2090 .unwrap_or_else(|| "None".to_string());
2091
2092 ui.add_llm_message(format!("Current model: {current_model}"));
2093 ui.add_llm_message("".to_string());
2094 ui.add_llm_message("Fetching available models...".to_string());
2095
2096 match self.llm.list_models().await {
2098 Ok(models) => {
2099 if models.is_empty() {
2100 ui.add_llm_message("No models found. Please pull a model first.".to_string());
2101 ui.add_llm_message("Example: ollama pull llama3.2".to_string());
2102 } else {
2103 ui.add_llm_message(format!("Available models ({}):", models.len()));
2104 for model in &models {
2105 if model == ¤t_model {
2106 ui.add_llm_message(format!(" * {model} (current)"));
2107 } else {
2108 ui.add_llm_message(format!(" {model}"));
2109 }
2110 }
2111 ui.add_llm_message("".to_string());
2112 ui.add_llm_message("To change model, use: /model <name>".to_string());
2113 }
2114 }
2115 Err(e) => {
2116 ui.add_llm_message(format!("Failed to fetch models: {e}"));
2117 ui.add_llm_message("Make sure Ollama is running.".to_string());
2118 }
2119 }
2120
2121 Ok(())
2122 }
2123
2124 async fn handle_change_model(&mut self, model: String, ui: &mut App) -> Result<()> {
2125 match self.llm.list_models().await {
2127 Ok(models) => {
2128 if models.contains(&model) {
2129 self.state.set_ollama_model(Some(model.clone())).await;
2130 ui.add_llm_message(format!("✓ Changed model to: {model}"));
2131 } else {
2132 ui.add_llm_message(format!("✗ Model '{model}' not found"));
2133 ui.add_llm_message("".to_string());
2134
2135 if models.is_empty() {
2136 ui.add_llm_message("No models available. Pull a model first:".to_string());
2137 ui.add_llm_message(" ollama pull llama3.2".to_string());
2138 } else {
2139 ui.add_llm_message("Available models:".to_string());
2140 for available_model in &models {
2141 ui.add_llm_message(format!(" {available_model}"));
2142 }
2143 ui.add_llm_message("".to_string());
2144 ui.add_llm_message("Or pull the model:".to_string());
2145 ui.add_llm_message(format!(" ollama pull {model}"));
2146 }
2147 }
2148 }
2149 Err(e) => {
2150 ui.add_llm_message(format!("Failed to validate model: {e}"));
2151 ui.add_llm_message("Make sure Ollama is running.".to_string());
2152 }
2153 }
2154
2155 Ok(())
2156 }
2157
2158 async fn handle_show_backend(&mut self, ui: &mut App) -> Result<()> {
2159 let backend_type = self.llm.backend_type();
2160 let backend_url = self.llm.backend_url();
2161 let current_model = self
2162 .state
2163 .get_ollama_model()
2164 .await
2165 .unwrap_or_else(|| "None".to_string());
2166
2167 ui.add_llm_message(format!("LLM Backend: {}", backend_type));
2168 ui.add_llm_message(format!(" URL: {}", backend_url));
2169 ui.add_llm_message(format!(" Model: {}", current_model));
2170 ui.add_llm_message("".to_string());
2171 ui.add_llm_message("To switch backend:".to_string());
2172 ui.add_llm_message(" /backend ollama [url] - Switch to Ollama".to_string());
2173 ui.add_llm_message(
2174 " /backend openai <url> [api-key] - Switch to OpenAI-compatible".to_string(),
2175 );
2176 Ok(())
2177 }
2178
2179 #[cfg(not(target_arch = "wasm32"))]
2180 async fn handle_set_backend(&mut self, args: String, ui: &mut App) -> Result<()> {
2181 let parts: Vec<&str> = args.splitn(3, ' ').collect();
2182
2183 match parts.first().map(|s| s.to_lowercase()).as_deref() {
2184 Some("ollama") => {
2185 let url = parts.get(1).unwrap_or(&"http://localhost:11434");
2186 let new_client = crate::llm::OllamaClient::new(url.to_string())
2187 .with_app_state(self.state.clone());
2188 self.llm = new_client.clone();
2189 self.state.set_llm_client(new_client).await;
2190 ui.add_llm_message(format!("✓ Switched to Ollama backend: {}", url));
2191 ui.add_llm_message(" Use /model to list and select a model.".to_string());
2192 }
2193 Some("openai") => {
2194 if parts.len() < 2 {
2195 ui.add_llm_message("✗ Usage: /backend openai <url> [api-key]".to_string());
2196 ui.add_llm_message(" The API key can also be set via NETGET_API_KEY or OPENAI_API_KEY env vars.".to_string());
2197 return Ok(());
2198 }
2199 let url = parts[1];
2200 let api_key = if parts.len() >= 3 {
2201 parts[2].to_string()
2202 } else {
2203 std::env::var("NETGET_API_KEY")
2205 .or_else(|_| std::env::var("OPENAI_API_KEY"))
2206 .unwrap_or_default()
2207 };
2208 if api_key.is_empty() {
2209 ui.add_llm_message("✗ API key required. Provide as third argument or set NETGET_API_KEY/OPENAI_API_KEY env var.".to_string());
2210 return Ok(());
2211 }
2212 let new_client = crate::llm::OllamaClient::new_openai(url, &api_key)
2213 .with_app_state(self.state.clone());
2214 self.llm = new_client.clone();
2215 self.state.set_llm_client(new_client).await;
2216 ui.add_llm_message(format!("✓ Switched to OpenAI-compatible backend: {}", url));
2217 ui.add_llm_message(" Use /model <name> to set the model.".to_string());
2218 }
2219 _ => {
2220 ui.add_llm_message("✗ Unknown backend. Use: /backend ollama [url] or /backend openai <url> [api-key]".to_string());
2221 }
2222 }
2223 Ok(())
2224 }
2225
2226 #[cfg(target_arch = "wasm32")]
2229 async fn handle_set_backend(&mut self, _args: String, ui: &mut App) -> Result<()> {
2230 ui.add_llm_message(
2231 "✗ The browser build talks to the page's model bridge; pick the model on the page."
2232 .to_string(),
2233 );
2234 Ok(())
2235 }
2236
2237 async fn handle_show_docs(&mut self, protocol: Option<String>, ui: &mut App) -> Result<()> {
2238 use crate::docs;
2239
2240 if let Some(protocol_name) = protocol {
2241 match docs::show_protocol_docs(&protocol_name) {
2243 Ok(docs_text) => {
2244 for line in docs_text.lines() {
2246 ui.add_llm_message(line.to_string());
2247 }
2248 }
2249 Err(err_msg) => {
2250 ui.add_llm_message(err_msg);
2251 }
2252 }
2253 } else {
2254 let docs_text = docs::list_all_protocols();
2256 for line in docs_text.lines() {
2257 ui.add_llm_message(line.to_string());
2258 }
2259 }
2260
2261 Ok(())
2262 }
2263
2264 async fn handle_show_environment(&mut self, ui: &mut App) -> Result<()> {
2266 use crate::protocol::{registry, CLIENT_REGISTRY};
2267
2268 ui.add_llm_message("".to_string());
2269 ui.add_llm_message("=== NetGet Environment ===".to_string());
2270 ui.add_llm_message("".to_string());
2271
2272 ui.add_llm_message("System Information:".to_string());
2274 ui.add_llm_message(format!(" OS: {}", std::env::consts::OS));
2275 ui.add_llm_message(format!(" Architecture: {}", std::env::consts::ARCH));
2276 ui.add_llm_message(format!(
2277 " Rust Version: {}",
2278 env!("CARGO_PKG_RUST_VERSION", "unknown")
2279 ));
2280 ui.add_llm_message(format!(" NetGet Version: {}", env!("CARGO_PKG_VERSION")));
2281 ui.add_llm_message("".to_string());
2282
2283 let model = self
2285 .state
2286 .get_ollama_model()
2287 .await
2288 .unwrap_or_else(|| "None".to_string());
2289 let web_search_mode = self.state.get_web_search_mode().await;
2290 let scripting_mode = self.state.get_selected_scripting_mode().await;
2291
2292 ui.add_llm_message("LLM Configuration:".to_string());
2293 ui.add_llm_message(format!(" Ollama Model: {}", model));
2294 ui.add_llm_message(format!(" Web Search: {}", web_search_mode.as_str()));
2295 ui.add_llm_message(format!(" Scripting: {}", scripting_mode.as_str()));
2296 ui.add_llm_message("".to_string());
2297
2298 let caps = self.state.get_system_capabilities().await;
2300
2301 let server_excluded = registry().get_excluded_protocols(&caps);
2303 let server_available = registry().get_available_protocols(&caps);
2304
2305 let client_excluded = CLIENT_REGISTRY.get_excluded_protocols(&caps);
2307 let client_available = CLIENT_REGISTRY.get_available_protocols(&caps);
2308
2309 ui.add_llm_message("System Capabilities:".to_string());
2311 ui.add_llm_message(format!(
2312 " Root Access: {}",
2313 if caps.is_root { "Yes" } else { "No" }
2314 ));
2315 ui.add_llm_message(format!(
2316 " Privileged Ports (<1024): {}",
2317 if caps.can_bind_privileged_ports {
2318 "Yes"
2319 } else {
2320 "No"
2321 }
2322 ));
2323 ui.add_llm_message(format!(
2324 " Raw Socket Access (pcap): {}",
2325 if caps.has_raw_socket_access {
2326 "Yes"
2327 } else {
2328 "No"
2329 }
2330 ));
2331 ui.add_llm_message("".to_string());
2332
2333 let total_server_protocols = server_available.len() + server_excluded.len();
2335 let total_client_protocols = client_available.len() + client_excluded.len();
2336
2337 ui.add_llm_message(format!(
2338 "Server Protocols: {} available, {} excluded (total {})",
2339 server_available.len(),
2340 server_excluded.len(),
2341 total_server_protocols
2342 ));
2343 ui.add_llm_message(format!(
2344 "Client Protocols: {} available, {} excluded (total {})",
2345 client_available.len(),
2346 client_excluded.len(),
2347 total_client_protocols
2348 ));
2349 ui.add_llm_message("".to_string());
2350
2351 if !server_excluded.is_empty() {
2353 ui.add_llm_message("Excluded Server Protocols:".to_string());
2354 let mut excluded_names: Vec<_> = server_excluded.keys().cloned().collect();
2355 excluded_names.sort();
2356
2357 for protocol_name in excluded_names {
2358 if let Some(missing_deps) = server_excluded.get(&protocol_name) {
2359 ui.add_llm_message(format!(" {}", protocol_name));
2360 for dep in missing_deps {
2361 ui.add_llm_message(format!(" ✗ {}: {}", dep.name(), dep.description()));
2362 ui.add_llm_message(format!(" → {}", dep.installation_hint()));
2363 }
2364 }
2365 }
2366 ui.add_llm_message("".to_string());
2367 }
2368
2369 if !client_excluded.is_empty() {
2370 ui.add_llm_message("Excluded Client Protocols:".to_string());
2371 let mut excluded_names: Vec<_> = client_excluded.keys().cloned().collect();
2372 excluded_names.sort();
2373
2374 for protocol_name in excluded_names {
2375 if let Some(missing_deps) = client_excluded.get(&protocol_name) {
2376 ui.add_llm_message(format!(" {}", protocol_name));
2377 for dep in missing_deps {
2378 ui.add_llm_message(format!(" ✗ {}: {}", dep.name(), dep.description()));
2379 ui.add_llm_message(format!(" → {}", dep.installation_hint()));
2380 }
2381 }
2382 }
2383 ui.add_llm_message("".to_string());
2384 }
2385
2386 if server_excluded.is_empty() && client_excluded.is_empty() {
2387 ui.add_llm_message("✓ All protocols are available!".to_string());
2388 ui.add_llm_message("".to_string());
2389 }
2390
2391 Ok(())
2392 }
2393
2394 async fn handle_save(&mut self, name: String, id: Option<u32>, ui: &mut App) -> Result<()> {
2395 use crate::state::client::ClientId;
2396 use crate::state::server::ServerId;
2397 use crate::utils::save_load;
2398
2399 let path = if let Some(id_val) = id {
2400 let server_id = ServerId::new(id_val);
2403 if self.state.get_server(server_id).await.is_some() {
2404 match save_load::save_server(&self.state, server_id, &name).await {
2405 Ok(path) => {
2406 ui.add_llm_message(format!(
2407 "[SAVE] Saved server #{} to: {}",
2408 id_val,
2409 path.display()
2410 ));
2411 path
2412 }
2413 Err(e) => {
2414 ui.add_llm_message(format!(
2415 "[ERROR] Failed to save server #{}: {}",
2416 id_val, e
2417 ));
2418 return Ok(());
2419 }
2420 }
2421 } else {
2422 let client_id = ClientId::new(id_val);
2424 if self.state.get_client(client_id).await.is_some() {
2425 match save_load::save_client(&self.state, client_id, &name).await {
2426 Ok(path) => {
2427 ui.add_llm_message(format!(
2428 "[SAVE] Saved client #{} to: {}",
2429 id_val,
2430 path.display()
2431 ));
2432 path
2433 }
2434 Err(e) => {
2435 ui.add_llm_message(format!(
2436 "[ERROR] Failed to save client #{}: {}",
2437 id_val, e
2438 ));
2439 return Ok(());
2440 }
2441 }
2442 } else {
2443 ui.add_llm_message(format!(
2444 "[ERROR] No server or client found with ID #{}",
2445 id_val
2446 ));
2447 return Ok(());
2448 }
2449 }
2450 } else {
2451 match save_load::save_all(&self.state, &name).await {
2453 Ok(path) => {
2454 let servers = self.state.get_all_servers().await;
2455 let clients = self.state.get_all_clients().await;
2456 ui.add_llm_message(format!(
2457 "[SAVE] Saved {} server(s) and {} client(s) to: {}",
2458 servers.len(),
2459 clients.len(),
2460 path.display()
2461 ));
2462 path
2463 }
2464 Err(e) => {
2465 ui.add_llm_message(format!("[ERROR] Failed to save configuration: {}", e));
2466 return Ok(());
2467 }
2468 }
2469 };
2470
2471 ui.add_llm_message(format!(
2472 "[INFO] Use '/load {}' to restore this configuration",
2473 path.display()
2474 ));
2475 Ok(())
2476 }
2477
2478 async fn handle_load(&mut self, name: String, ui: &mut App) -> Result<()> {
2479 use crate::utils::save_load;
2480
2481 let actions = match save_load::load_actions(&name).await {
2483 Ok(actions) => actions,
2484 Err(e) => {
2485 ui.add_llm_message(format!("[ERROR] Failed to load file '{}': {}", name, e));
2486 return Ok(());
2487 }
2488 };
2489
2490 if actions.is_empty() {
2491 ui.add_llm_message(format!("[WARN] File '{}' contains no actions", name));
2492 return Ok(());
2493 }
2494
2495 ui.add_llm_message(format!(
2496 "[LOAD] Loading {} action(s) from: {}",
2497 actions.len(),
2498 save_load::normalize_filename(&name)
2499 ));
2500
2501 for (i, action) in actions.iter().enumerate() {
2503 if action.get("type").and_then(serde_json::Value::as_str) == Some("restore_session") {
2504 match save_load::restore_session(&self.state, &self.llm, &action["session"]).await {
2505 Ok(()) => ui.add_llm_message("[LOAD] Restored session relationships".into()),
2506 Err(error) => {
2507 ui.add_llm_message(format!("[ERROR] Session restoration failed: {error:#}"))
2508 }
2509 }
2510 continue;
2511 }
2512
2513 if let Ok(common_action) = crate::llm::actions::common::CommonAction::from_json(action)
2515 {
2516 use crate::llm::actions::common::CommonAction;
2517
2518 match common_action {
2519 CommonAction::OpenServer {
2520 mac_address,
2521 interface,
2522 host,
2523 port,
2524 protocol,
2525 send_first,
2526 initial_memory,
2527 instruction,
2528 startup_params,
2529 event_handlers,
2530 scheduled_tasks,
2531 feedback_instructions,
2532 } => {
2533 let (status_tx, _status_rx) = mpsc::unbounded_channel();
2536
2537 match server_startup::start_server_from_action(
2539 &self.state,
2540 mac_address,
2541 interface.clone(),
2542 host,
2543 port,
2544 &protocol,
2545 send_first,
2546 initial_memory,
2547 instruction.clone(),
2548 startup_params,
2549 event_handlers,
2550 scheduled_tasks,
2551 feedback_instructions,
2552 status_tx,
2553 )
2554 .await
2555 {
2556 Ok(server_id) => {
2557 let binding_desc = if let Some(iface) = &interface {
2558 format!("interface {} ({})", iface, protocol)
2559 } else if let Some(p) = port {
2560 format!("port {} ({})", p, protocol)
2561 } else {
2562 format!("({})", protocol)
2563 };
2564 ui.add_llm_message(format!(
2565 "[LOAD] Opened server #{} on {}",
2566 server_id.as_u32(),
2567 binding_desc
2568 ));
2569 }
2570 Err(e) => {
2571 ui.add_llm_message(format!(
2572 "[ERROR] Failed to open server (action {}): {}",
2573 i + 1,
2574 e
2575 ));
2576 }
2577 }
2578 }
2579 CommonAction::OpenClient {
2580 protocol,
2581 remote_addr,
2582 instruction,
2583 startup_params,
2584 initial_memory,
2585 event_handlers,
2586 scheduled_tasks,
2587 feedback_instructions,
2588 } => {
2589 use crate::cli::client_startup;
2591
2592 match client_startup::start_client_from_action(
2593 &self.state,
2594 &protocol,
2595 &remote_addr,
2596 instruction.clone(),
2597 startup_params,
2598 initial_memory,
2599 event_handlers,
2600 scheduled_tasks,
2601 feedback_instructions,
2602 self.llm.clone(),
2603 None, )
2605 .await
2606 {
2607 Ok(client_id) => {
2608 ui.add_llm_message(format!(
2609 "[LOAD] Opened client #{} to {} ({})",
2610 client_id.as_u32(),
2611 remote_addr,
2612 protocol
2613 ));
2614 }
2615 Err(e) => {
2616 ui.add_llm_message(format!(
2617 "[ERROR] Failed to open client (action {}): {}",
2618 i + 1,
2619 e
2620 ));
2621 }
2622 }
2623 }
2624 _ => {
2625 ui.add_llm_message(format!(
2626 "[WARN] Skipping unsupported action type (action {})",
2627 i + 1
2628 ));
2629 }
2630 }
2631 } else {
2632 ui.add_llm_message(format!("[WARN] Skipping invalid action (action {})", i + 1));
2633 }
2634 }
2635
2636 ui.add_llm_message("[LOAD] Configuration loaded successfully".to_string());
2637 Ok(())
2638 }
2639
2640 #[cfg(feature = "sqlite")]
2641 async fn handle_sqlite(
2642 &mut self,
2643 db_id: Option<u32>,
2644 query: Option<String>,
2645 ui: &mut App,
2646 ) -> Result<()> {
2647 match (db_id, query) {
2648 (None, None) => {
2649 let databases = self.state.get_all_databases().await;
2651 if databases.is_empty() {
2652 ui.add_llm_message("[DB] No databases".to_string());
2653 } else {
2654 ui.add_llm_message(format!("[DB] {} database(s):", databases.len()));
2655 for db in databases {
2656 ui.add_llm_message(format!(" {}", db.schema_summary()));
2657 }
2658 }
2659 }
2660 (Some(id), None) => {
2661 let db_id_obj = crate::state::DatabaseId::new(id);
2663 if let Some(db) = self.state.get_database(db_id_obj).await {
2664 ui.add_llm_message(format!("[DB] Database {}:", db_id_obj));
2665 ui.add_llm_message(db.schema_summary());
2666 } else {
2667 ui.add_llm_message(format!("[ERROR] Database {} not found", db_id_obj));
2668 }
2669 }
2670 (Some(id), Some(sql)) => {
2671 let db_id_obj = crate::state::DatabaseId::new(id);
2673 match self.state.execute_sql(db_id_obj, &sql).await {
2674 Ok(result) => {
2675 let formatted = result.format();
2676 ui.add_llm_message(format!("[DB] Query result:\n{}", formatted));
2677 }
2678 Err(e) => {
2679 ui.add_llm_message(format!("[ERROR] SQL error: {}", e));
2680 }
2681 }
2682 }
2683 (None, Some(sql)) => {
2684 let databases = self.state.get_all_databases().await;
2686 if databases.is_empty() {
2687 ui.add_llm_message("[ERROR] No databases available".to_string());
2688 } else {
2689 let db_id = databases[0].id;
2690 match self.state.execute_sql(db_id, &sql).await {
2691 Ok(result) => {
2692 let formatted = result.format();
2693 ui.add_llm_message(format!("[DB] Query result:\n{}", formatted));
2694 }
2695 Err(e) => {
2696 ui.add_llm_message(format!("[ERROR] SQL error: {}", e));
2697 }
2698 }
2699 }
2700 }
2701 }
2702 Ok(())
2703 }
2704}
2705
2706const COMMON_ACTION_NAMES: &[&str] = &[
2715 "show_message",
2716 "open_server",
2717 "close_server",
2718 "close_all_servers",
2719 "open_client",
2720 "close_client",
2721 "close_all_clients",
2722 "close_connection_by_id",
2723 "reconnect_client",
2724 "update_client_instruction",
2725 "update_instruction",
2726 "change_model",
2727 "set_memory",
2728 "append_memory",
2729 "append_to_log",
2730 "schedule_task",
2731 "cancel_task",
2732 "provide_feedback",
2733 #[cfg(feature = "sqlite")]
2734 "create_database",
2735 #[cfg(feature = "sqlite")]
2736 "delete_database",
2737];
2738
2739pub fn common_action_names() -> &'static [&'static str] {
2744 COMMON_ACTION_NAMES
2745}
2746
2747const MAX_LISTED_ACTIONS: usize = 40;
2749
2750struct ActionCatalog {
2753 names: std::collections::BTreeSet<String>,
2755 scope: String,
2757}
2758
2759fn describe_pattern(pattern: &crate::scripting::EventPattern) -> String {
2761 use crate::scripting::EventPattern;
2762 match pattern {
2763 EventPattern::Specific(id) => format!("event '{}'", id),
2764 EventPattern::Wildcard => "the wildcard event pattern".to_string(),
2765 }
2766}
2767
2768fn server_actions_for_pattern(
2785 protocol: &dyn crate::llm::actions::protocol_trait::Protocol,
2786 pattern: &crate::scripting::EventPattern,
2787) -> Option<Vec<String>> {
2788 let event_types = protocol.get_event_types();
2789 let matching: Vec<_> = event_types
2790 .iter()
2791 .filter(|et| pattern.matches(&et.id))
2792 .collect();
2793 if matching.is_empty() {
2794 return None;
2795 }
2796
2797 let mut names: Vec<String> = protocol
2798 .get_sync_actions()
2799 .into_iter()
2800 .map(|a| a.name)
2801 .collect();
2802 for event_type in matching {
2803 names.extend(event_type.actions.iter().map(|a| a.name.clone()));
2804 }
2805 Some(names)
2806}
2807
2808fn event_handler_parameter_name(error_msg: &str) -> &'static str {
2820 if error_msg.contains("instruction") {
2821 "event_handlers[].handler.instruction"
2822 } else if error_msg.contains("event_handlers") {
2823 "event_handlers"
2824 } else {
2825 "unknown"
2826 }
2827}
2828
2829fn catalog_state() -> &'static AppState {
2843 static CATALOG_STATE: std::sync::OnceLock<AppState> = std::sync::OnceLock::new();
2844 CATALOG_STATE.get_or_init(AppState::new)
2845}
2846
2847fn action_catalog_for_pattern(pattern: &crate::scripting::EventPattern) -> ActionCatalog {
2867 let mut names: std::collections::BTreeSet<String> = COMMON_ACTION_NAMES
2868 .iter()
2869 .map(|s| (*s).to_string())
2870 .collect();
2871 let mut scopes: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2872
2873 for (_, protocol) in crate::protocol::server_registry::registry().all_protocols() {
2874 if let Some(actions) = server_actions_for_pattern(protocol.as_ref(), pattern) {
2875 scopes.insert(protocol.protocol_name().to_string());
2876 names.extend(actions);
2877 }
2878 }
2879
2880 for protocol in crate::protocol::client_registry::CLIENT_REGISTRY.get_all() {
2881 if let Some(actions) = crate::llm::actions::client_trait::client_action_names_for_pattern(
2882 protocol.as_ref(),
2883 catalog_state(),
2884 pattern,
2885 ) {
2886 scopes.insert(protocol.protocol_name().to_string());
2887 names.extend(actions);
2888 }
2889 }
2890
2891 if scopes.is_empty() {
2892 return ActionCatalog {
2894 names: std::collections::BTreeSet::new(),
2895 scope: String::new(),
2896 };
2897 }
2898
2899 let scope = scopes.into_iter().collect::<Vec<_>>().join(", ");
2900 ActionCatalog { names, scope }
2901}
2902
2903fn validate_static_action_names(
2908 pattern: &crate::scripting::EventPattern,
2909 actions: &[serde_json::Value],
2910) -> Result<()> {
2911 let catalog = action_catalog_for_pattern(pattern);
2912 if catalog.names.is_empty() {
2913 return Ok(());
2915 }
2916
2917 for action in actions {
2918 let Some(name) = action.get("type").and_then(|v| v.as_str()) else {
2919 anyhow::bail!(
2920 "Static handler action for {} has no \"type\" field: {}",
2921 describe_pattern(pattern),
2922 action
2923 );
2924 };
2925
2926 if catalog.names.contains(name) {
2927 continue;
2928 }
2929
2930 let listed: Vec<&str> = catalog
2931 .names
2932 .iter()
2933 .take(MAX_LISTED_ACTIONS)
2934 .map(|s| s.as_str())
2935 .collect();
2936 let overflow = catalog.names.len().saturating_sub(listed.len());
2937 let mut valid = listed.join(", ");
2938 if overflow > 0 {
2939 valid.push_str(&format!(", … ({} more)", overflow));
2940 }
2941
2942 anyhow::bail!(
2943 "Unknown action \"{}\" in the static handler for {}. \
2944 Valid actions for {}: {}",
2945 name,
2946 describe_pattern(pattern),
2947 catalog.scope,
2948 valid
2949 );
2950 }
2951
2952 Ok(())
2953}