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