bamboo_agent/server/handlers/
command.rs1use crate::agent::skill::SkillDefinition;
2use crate::server::app_state::AppState;
3use crate::server::error::AppError;
4use actix_web::{web, HttpResponse};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
9#[serde(rename_all = "lowercase")]
10pub enum CommandType {
11 Workflow,
13 Skill,
15 Mcp,
17}
18
19#[derive(Debug, Serialize)]
21pub struct CommandItem {
22 pub id: String,
24 pub name: String,
26 pub display_name: String,
28 pub description: String,
30 #[serde(rename = "type")]
32 pub command_type: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub category: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub tags: Option<Vec<String>>,
39 pub metadata: serde_json::Value,
41}
42
43#[derive(Debug, Serialize)]
45pub struct CommandListResponse {
46 pub commands: Vec<CommandItem>,
48 pub total: usize,
50}
51
52pub async fn list_commands(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
83 let mut commands = Vec::new();
84
85 match list_workflows_as_commands(&app_state.app_data_dir).await {
87 Ok(workflows) => commands.extend(workflows),
88 Err(e) => {
89 log::warn!("Failed to load workflows: {}", e);
90 }
92 }
93
94 let skills = app_state
96 .skill_manager
97 .store()
98 .list_skills(None, false)
99 .await;
100
101 let skill_commands: Vec<CommandItem> = skills
102 .into_iter()
103 .map(|skill| skill_to_command(&skill))
104 .collect();
105 commands.extend(skill_commands);
106
107 match list_mcp_tools_as_commands(app_state.get_ref()).await {
109 Ok(mcp_tools) => commands.extend(mcp_tools),
110 Err(e) => {
111 log::warn!("Failed to load MCP tools: {}", e);
112 }
113 }
114
115 commands.sort_by(|a, b| a.name.cmp(&b.name));
117
118 Ok(HttpResponse::Ok().json(CommandListResponse {
119 total: commands.len(),
120 commands,
121 }))
122}
123
124pub async fn get_command(
154 app_state: web::Data<AppState>,
155 path: web::Path<(String, String)>,
156) -> Result<HttpResponse, AppError> {
157 let (command_type, id) = path.into_inner();
158
159 match command_type.as_str() {
160 "workflow" => {
161 let workflows_dir = app_state.app_data_dir.join("workflows");
163 let filename = format!("{}.md", id);
164 let filepath = workflows_dir.join(&filename);
165
166 if !filepath.exists() {
167 return Err(AppError::NotFound(format!("Workflow {} not found", id)));
168 }
169
170 let content = tokio::fs::read_to_string(&filepath).await.map_err(|e| {
171 AppError::InternalError(anyhow::anyhow!("Failed to read workflow: {}", e))
172 })?;
173
174 Ok(HttpResponse::Ok().json(serde_json::json!({
175 "id": format!("workflow-{}", id),
176 "name": id,
177 "content": content,
178 "type": "workflow"
179 })))
180 }
181 "skill" => match app_state.skill_manager.store().get_skill(&id).await {
182 Ok(skill) => Ok(HttpResponse::Ok().json(skill)),
183 Err(e) => Err(AppError::NotFound(format!("Skill {} not found: {}", id, e))),
184 },
185 "mcp" => {
186 Err(AppError::NotFound(
188 "MCP tools do not support content retrieval".to_string(),
189 ))
190 }
191 _ => Err(AppError::NotFound(format!(
192 "Unknown command type: {}",
193 command_type
194 ))),
195 }
196}
197
198async fn list_workflows_as_commands(
202 data_dir: &std::path::Path,
203) -> Result<Vec<CommandItem>, AppError> {
204 let dir = data_dir.join("workflows");
205 tokio::fs::create_dir_all(&dir).await.map_err(|e| {
206 AppError::InternalError(anyhow::anyhow!("Failed to create workflows dir: {}", e))
207 })?;
208
209 let mut entries = tokio::fs::read_dir(&dir).await.map_err(|e| {
210 AppError::InternalError(anyhow::anyhow!("Failed to read workflows dir: {}", e))
211 })?;
212
213 let mut commands = Vec::new();
214
215 while let Some(entry) = entries
216 .next_entry()
217 .await
218 .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to read entry: {}", e)))?
219 {
220 let path = entry.path();
221 if path.extension().and_then(|s| s.to_str()) != Some("md") {
222 continue;
223 }
224
225 let name = path
226 .file_stem()
227 .and_then(|s| s.to_str())
228 .unwrap_or_default()
229 .to_string();
230
231 if name.is_empty() {
232 continue;
233 }
234
235 let metadata = entry.metadata().await.map_err(|e| {
236 AppError::InternalError(anyhow::anyhow!("Failed to read metadata: {}", e))
237 })?;
238
239 let filename = path
240 .file_name()
241 .and_then(|s| s.to_str())
242 .unwrap_or_default()
243 .to_string();
244
245 commands.push(CommandItem {
246 id: format!("workflow-{}", name),
247 name: name.clone(),
248 display_name: name.clone(),
249 description: format!("Workflow: {}", name),
250 command_type: "workflow".to_string(),
251 category: None,
252 tags: None,
253 metadata: serde_json::json!({
254 "filename": filename,
255 "size": metadata.len(),
256 "source": "global"
257 }),
258 });
259 }
260
261 Ok(commands)
262}
263
264fn skill_to_command(skill: &SkillDefinition) -> CommandItem {
266 CommandItem {
267 id: format!("skill-{}", skill.id),
268 name: skill.id.clone(),
269 display_name: skill.name.clone(),
270 description: skill.description.clone(),
271 command_type: "skill".to_string(),
272 category: Some(skill.category.clone()),
273 tags: Some(skill.tags.clone()),
274 metadata: serde_json::json!({
275 "prompt": skill.prompt,
276 "toolRefs": skill.tool_refs,
277 "workflowRefs": skill.workflow_refs,
278 "visibility": skill.visibility,
279 }),
280 }
281}
282
283async fn list_mcp_tools_as_commands(state: &AppState) -> Result<Vec<CommandItem>, AppError> {
287 let aliases = state.mcp_manager.tool_index().all_aliases();
288
289 let commands: Vec<CommandItem> = aliases
290 .into_iter()
291 .filter_map(|alias| {
292 state
293 .mcp_manager
294 .get_tool_info(&alias.server_id, &alias.original_name)
295 .map(|tool| CommandItem {
296 id: format!("mcp-{}-{}", alias.server_id, alias.original_name),
297 name: alias.alias.clone(),
298 display_name: alias.alias.clone(),
299 description: tool.description.clone(),
300 command_type: "mcp".to_string(),
301 category: Some("MCP Tools".to_string()),
302 tags: None,
303 metadata: serde_json::json!({
304 "serverId": alias.server_id,
305 "originalName": alias.original_name,
306 }),
307 })
308 })
309 .collect();
310
311 Ok(commands)
312}
313
314pub fn config(cfg: &mut web::ServiceConfig) {
320 cfg.route("/commands", web::get().to(list_commands))
321 .route("/commands/{command_type}/{id}", web::get().to(get_command));
322}