Skip to main content

elizaos_plugin_shell/actions/
execute_command.rs

1use crate::{Action, ActionExample, ActionResult, ShellService};
2use async_trait::async_trait;
3use serde_json::{json, Value};
4
5pub struct ExecuteCommandAction;
6
7impl ExecuteCommandAction {
8    const COMMAND_KEYWORDS: &'static [&'static str] = &[
9        "run",
10        "execute",
11        "command",
12        "shell",
13        "install",
14        "brew",
15        "npm",
16        "create",
17        "file",
18        "directory",
19        "folder",
20        "list",
21        "show",
22        "system",
23        "info",
24        "check",
25        "status",
26        "cd",
27        "ls",
28        "mkdir",
29        "echo",
30        "cat",
31        "touch",
32        "git",
33        "build",
34        "test",
35    ];
36
37    /// Check if text contains command keywords.
38    fn has_command_keyword(text: &str) -> bool {
39        let lower = text.to_lowercase();
40        Self::COMMAND_KEYWORDS.iter().any(|kw| lower.contains(kw))
41    }
42
43    /// Check if text starts with a direct command.
44    fn has_direct_command(text: &str) -> bool {
45        let direct_commands = [
46            "brew", "npm", "apt", "git", "ls", "cd", "echo", "cat", "touch", "mkdir", "rm", "mv",
47            "cp",
48        ];
49        let lower = text.to_lowercase();
50        direct_commands.iter().any(|cmd| {
51            lower.starts_with(cmd)
52                && (lower.len() == cmd.len() || lower.chars().nth(cmd.len()) == Some(' '))
53        })
54    }
55}
56
57#[async_trait]
58impl Action for ExecuteCommandAction {
59    fn name(&self) -> &str {
60        "EXECUTE_COMMAND"
61    }
62
63    fn similes(&self) -> Vec<&str> {
64        vec![
65            "RUN_COMMAND",
66            "SHELL_COMMAND",
67            "TERMINAL_COMMAND",
68            "EXEC",
69            "RUN",
70            "EXECUTE",
71            "CREATE_FILE",
72            "WRITE_FILE",
73            "MAKE_FILE",
74            "INSTALL",
75            "BREW_INSTALL",
76            "NPM_INSTALL",
77            "APT_INSTALL",
78        ]
79    }
80
81    fn description(&self) -> &str {
82        "Execute shell commands including brew install, npm install, apt-get, \
83         system commands, file operations, directory navigation, and scripts."
84    }
85
86    async fn validate(&self, message: &Value, _state: &Value) -> bool {
87        let text = message
88            .get("content")
89            .and_then(|c| c.get("text"))
90            .and_then(|t| t.as_str())
91            .unwrap_or("");
92
93        Self::has_command_keyword(text) || Self::has_direct_command(text)
94    }
95
96    async fn handler(
97        &self,
98        message: &Value,
99        _state: &Value,
100        service: Option<&mut ShellService>,
101    ) -> ActionResult {
102        let service = match service {
103            Some(s) => s,
104            None => {
105                return ActionResult {
106                    success: false,
107                    text: "Shell service is not available.".to_string(),
108                    data: None,
109                    error: Some("Shell service is not available".to_string()),
110                }
111            }
112        };
113
114        let text = message
115            .get("content")
116            .and_then(|c| c.get("text"))
117            .and_then(|t| t.as_str())
118            .unwrap_or("");
119
120        let command = extract_command_from_text(text);
121
122        if command.is_empty() {
123            return ActionResult {
124                success: false,
125                text:
126                    "Could not determine which command to execute. Please specify a shell command."
127                        .to_string(),
128                data: None,
129                error: Some("Could not extract command".to_string()),
130            };
131        }
132
133        let conversation_id = message
134            .get("room_id")
135            .and_then(|r| r.as_str())
136            .or_else(|| message.get("agent_id").and_then(|a| a.as_str()));
137
138        match service.execute_command(&command, conversation_id).await {
139            Ok(result) => {
140                let response_text = if result.success {
141                    let output = if result.stdout.is_empty() {
142                        "Command completed with no output.".to_string()
143                    } else {
144                        format!("Output:\n```\n{}\n```", result.stdout)
145                    };
146                    format!(
147                        "Command executed successfully in {}\n\n{}",
148                        result.executed_in, output
149                    )
150                } else {
151                    let exit_code_str = result
152                        .exit_code
153                        .map(|c| c.to_string())
154                        .unwrap_or_else(|| String::from("unknown"));
155                    let mut msg = format!(
156                        "Command failed with exit code {} in {}\n\n",
157                        exit_code_str, result.executed_in
158                    );
159                    if !result.stderr.is_empty() {
160                        msg.push_str(&format!("Error output:\n```\n{}\n```", result.stderr));
161                    }
162                    msg
163                };
164
165                ActionResult {
166                    success: result.success,
167                    text: response_text,
168                    data: Some(json!({
169                        "command": command,
170                        "exit_code": result.exit_code,
171                        "stdout": result.stdout,
172                        "stderr": result.stderr,
173                    })),
174                    error: if result.success {
175                        None
176                    } else {
177                        Some(result.stderr)
178                    },
179                }
180            }
181            Err(e) => ActionResult {
182                success: false,
183                text: format!("Failed to execute command: {}", e),
184                data: None,
185                error: Some(e.to_string()),
186            },
187        }
188    }
189
190    fn examples(&self) -> Vec<ActionExample> {
191        vec![
192            ActionExample {
193                user_message: "run ls -la".to_string(),
194                agent_response: "I'll execute that command for you.".to_string(),
195            },
196            ActionExample {
197                user_message: "show me what files are in this directory".to_string(),
198                agent_response: "I'll list the files in the current directory.".to_string(),
199            },
200            ActionExample {
201                user_message: "check the git status".to_string(),
202                agent_response: "I'll check the git repository status.".to_string(),
203            },
204            ActionExample {
205                user_message: "create a file called hello.txt".to_string(),
206                agent_response: "I'll create hello.txt for you.".to_string(),
207            },
208        ]
209    }
210}
211
212fn extract_command_from_text(text: &str) -> String {
213    let lower = text.to_lowercase();
214
215    let direct_commands = [
216        "ls", "cd", "pwd", "echo", "cat", "mkdir", "rm", "mv", "cp", "git", "npm", "brew", "apt",
217    ];
218
219    for cmd in direct_commands {
220        if lower.starts_with(cmd) {
221            return text.to_string();
222        }
223        if let Some(pos) = lower.find(&format!("run {}", cmd)) {
224            return text[pos + 4..].trim().to_string();
225        }
226        if let Some(pos) = lower.find(&format!("execute {}", cmd)) {
227            return text[pos + 8..].trim().to_string();
228        }
229    }
230
231    if let Some(pos) = lower.find("run ") {
232        return text[pos + 4..].trim().to_string();
233    }
234    if let Some(pos) = lower.find("execute ") {
235        return text[pos + 8..].trim().to_string();
236    }
237
238    if lower.contains("list") && (lower.contains("file") || lower.contains("director")) {
239        return "ls -la".to_string();
240    }
241    if lower.contains("git status") || (lower.contains("check") && lower.contains("git")) {
242        return "git status".to_string();
243    }
244    if lower.contains("current director") || lower.contains("where am i") {
245        return "pwd".to_string();
246    }
247
248    String::new()
249}