elizaos_plugin_shell/providers/
shell_history.rs1use 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!("{:.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}