Skip to main content

elizaos_plugin_shell/providers/
shell_history.rs

1use crate::{Provider, ProviderResult, ShellService};
2use async_trait::async_trait;
3use serde_json::{json, Value};
4use std::time::{Duration, UNIX_EPOCH};
5
6const MAX_OUTPUT_LENGTH: usize = 8000;
7const TRUNCATE_SEGMENT_LENGTH: usize = 4000;
8
9pub struct ShellHistoryProvider;
10
11fn format_timestamp(timestamp: f64) -> String {
12    let duration = Duration::from_secs_f64(timestamp);
13    let _datetime = UNIX_EPOCH + duration;
14    // Format as simple ISO-like string
15    format!("{:.0}", timestamp)
16}
17
18#[async_trait]
19impl Provider for ShellHistoryProvider {
20    fn name(&self) -> &str {
21        "SHELL_HISTORY"
22    }
23
24    fn description(&self) -> &str {
25        "Provides recent shell command history, current working directory, \
26         and file operations within the restricted environment"
27    }
28
29    fn position(&self) -> i32 {
30        99
31    }
32
33    async fn get(
34        &self,
35        message: &Value,
36        _state: &Value,
37        service: Option<&ShellService>,
38    ) -> ProviderResult {
39        let service = match service {
40            Some(s) => s,
41            None => {
42                return ProviderResult {
43                    values: json!({
44                        "shellHistory": "Shell service is not available",
45                        "currentWorkingDirectory": "N/A",
46                        "allowedDirectory": "N/A",
47                    }),
48                    text: "# Shell Status\n\nShell service is not available".to_string(),
49                    data: json!({
50                        "historyCount": 0,
51                        "cwd": "N/A",
52                        "allowedDir": "N/A",
53                    }),
54                };
55            }
56        };
57
58        let conversation_id = message
59            .get("room_id")
60            .and_then(|r| r.as_str())
61            .or_else(|| message.get("agent_id").and_then(|a| a.as_str()))
62            .unwrap_or("default");
63
64        let history = service.get_command_history(conversation_id, Some(10));
65        let cwd = service.get_current_directory(None);
66        let allowed_dir = service.get_allowed_directory();
67
68        let history_text = if history.is_empty() {
69            "No commands in history.".to_string()
70        } else {
71            history
72                .iter()
73                .map(|entry| {
74                    let mut entry_str = format!(
75                        "[{}] {}> {}",
76                        format_timestamp(entry.timestamp),
77                        entry.working_directory,
78                        entry.command
79                    );
80
81                    if !entry.stdout.is_empty() {
82                        let stdout = if entry.stdout.len() > MAX_OUTPUT_LENGTH {
83                            format!(
84                                "{}\n  ... [TRUNCATED] ...\n  {}",
85                                &entry.stdout[..TRUNCATE_SEGMENT_LENGTH],
86                                &entry.stdout[entry.stdout.len() - TRUNCATE_SEGMENT_LENGTH..]
87                            )
88                        } else {
89                            entry.stdout.clone()
90                        };
91                        entry_str.push_str(&format!("\n  Output: {}", stdout));
92                    }
93
94                    if !entry.stderr.is_empty() {
95                        let stderr = if entry.stderr.len() > MAX_OUTPUT_LENGTH {
96                            format!(
97                                "{}\n  ... [TRUNCATED] ...\n  {}",
98                                &entry.stderr[..TRUNCATE_SEGMENT_LENGTH],
99                                &entry.stderr[entry.stderr.len() - TRUNCATE_SEGMENT_LENGTH..]
100                            )
101                        } else {
102                            entry.stderr.clone()
103                        };
104                        entry_str.push_str(&format!("\n  Error: {}", stderr));
105                    }
106
107                    if let Some(exit_code) = entry.exit_code {
108                        entry_str.push_str(&format!("\n  Exit Code: {}", exit_code));
109                    }
110
111                    if let Some(ref file_ops) = entry.file_operations {
112                        if !file_ops.is_empty() {
113                            entry_str.push_str("\n  File Operations:");
114                            for op in file_ops {
115                                if let Some(ref secondary) = op.secondary_target {
116                                    entry_str.push_str(&format!(
117                                        "\n    - {:?}: {} → {}",
118                                        op.op_type, op.target, secondary
119                                    ));
120                                } else {
121                                    entry_str.push_str(&format!(
122                                        "\n    - {:?}: {}",
123                                        op.op_type, op.target
124                                    ));
125                                }
126                            }
127                        }
128                    }
129
130                    entry_str
131                })
132                .collect::<Vec<_>>()
133                .join("\n\n")
134        };
135
136        let recent_file_ops: Vec<_> = history
137            .iter()
138            .filter_map(|e| e.file_operations.as_ref())
139            .flat_map(|ops| ops.iter())
140            .take(5)
141            .collect();
142
143        let file_ops_text = if recent_file_ops.is_empty() {
144            String::new()
145        } else {
146            let ops_str = recent_file_ops
147                .iter()
148                .map(|op| {
149                    if let Some(ref secondary) = op.secondary_target {
150                        format!("- {:?}: {} → {}", op.op_type, op.target, secondary)
151                    } else {
152                        format!("- {:?}: {}", op.op_type, op.target)
153                    }
154                })
155                .collect::<Vec<_>>()
156                .join("\n");
157            format!("\n\n# Recent File Operations\n\n{}", ops_str)
158        };
159
160        let cwd_str = cwd.display().to_string();
161        let allowed_dir_str = allowed_dir.display().to_string();
162
163        let text = format!(
164            "Current Directory: {}\nAllowed Directory: {}\n\n# Shell History (Last 10)\n\n{}{}",
165            cwd_str, allowed_dir_str, history_text, file_ops_text
166        );
167
168        ProviderResult {
169            values: json!({
170                "shellHistory": history_text,
171                "currentWorkingDirectory": cwd_str,
172                "allowedDirectory": allowed_dir_str,
173            }),
174            text,
175            data: json!({
176                "historyCount": history.len(),
177                "cwd": cwd_str,
178                "allowedDir": allowed_dir_str,
179            }),
180        }
181    }
182}