elizaos_plugin_shell/actions/
execute_command.rs1use 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 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 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}