Skip to main content

bamboo_server/handlers/command/
handlers.rs

1use actix_web::{web, HttpResponse};
2
3use crate::app_state::AppState;
4use crate::error::AppError;
5use crate::handlers::settings::is_safe_workflow_name;
6
7use super::sources::{list_mcp_tools_as_commands, list_workflows_as_commands, skill_to_command};
8use super::types::CommandListResponse;
9
10/// Lists all available commands from workflows, skills, and MCP tools.
11pub async fn list_commands(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
12    let mut commands = Vec::new();
13
14    match list_workflows_as_commands(&app_state.app_data_dir).await {
15        Ok(workflows) => commands.extend(workflows),
16        Err(error) => {
17            tracing::warn!("Failed to load workflows: {error}");
18        }
19    }
20
21    let skills = app_state
22        .skill_manager
23        .store()
24        .list_skills(None, false)
25        .await;
26    let skill_commands = skills.into_iter().map(|skill| skill_to_command(&skill));
27    commands.extend(skill_commands);
28
29    match list_mcp_tools_as_commands(app_state.get_ref()).await {
30        Ok(mcp_tools) => commands.extend(mcp_tools),
31        Err(error) => {
32            tracing::warn!("Failed to load MCP tools: {error}");
33        }
34    }
35
36    commands.sort_by(|left, right| left.name.cmp(&right.name));
37    Ok(HttpResponse::Ok().json(CommandListResponse {
38        total: commands.len(),
39        commands,
40    }))
41}
42
43/// Retrieves a specific command by type and ID.
44pub async fn get_command(
45    app_state: web::Data<AppState>,
46    path: web::Path<(String, String)>,
47) -> Result<HttpResponse, AppError> {
48    let (command_type, id) = path.into_inner();
49
50    match command_type.as_str() {
51        "workflow" => {
52            if !is_safe_workflow_name(&id) {
53                return Err(AppError::BadRequest("Invalid workflow name".to_string()));
54            }
55
56            let workflows_dir = app_state.app_data_dir.join("workflows");
57            let filename = format!("{id}.md");
58            let filepath = workflows_dir.join(&filename);
59
60            if !filepath.exists() {
61                return Err(AppError::NotFound(format!("Workflow {id} not found")));
62            }
63
64            let content = tokio::fs::read_to_string(&filepath)
65                .await
66                .map_err(|error| {
67                    AppError::InternalError(anyhow::anyhow!("Failed to read workflow: {error}"))
68                })?;
69
70            Ok(HttpResponse::Ok().json(serde_json::json!({
71                "id": format!("workflow-{id}"),
72                "name": id,
73                "content": content,
74                "type": "workflow"
75            })))
76        }
77        "skill" => match app_state.skill_manager.store().get_skill(&id).await {
78            Ok(skill) => Ok(HttpResponse::Ok().json(skill)),
79            Err(error) => Err(AppError::NotFound(format!("Skill {id} not found: {error}"))),
80        },
81        "mcp" => Err(AppError::NotFound(
82            "MCP tools do not support content retrieval".to_string(),
83        )),
84        _ => Err(AppError::NotFound(format!(
85            "Unknown command type: {command_type}"
86        ))),
87    }
88}
89
90/// Configures command-related routes.
91pub fn config(cfg: &mut web::ServiceConfig) {
92    cfg.route("/commands", web::get().to(list_commands))
93        .route("/commands/{command_type}/{id}", web::get().to(get_command));
94}