bamboo-server 2026.4.26

HTTP server and API layer for the Bamboo agent framework
Documentation
use std::path::Path;

use crate::app_state::AppState;
use crate::error::AppError;
use bamboo_engine::SkillDefinition;

use super::types::CommandItem;

pub(super) async fn list_workflows_as_commands(
    data_dir: &Path,
) -> Result<Vec<CommandItem>, AppError> {
    let dir = data_dir.join("workflows");
    tokio::fs::create_dir_all(&dir).await.map_err(|error| {
        AppError::InternalError(anyhow::anyhow!("Failed to create workflows dir: {error}"))
    })?;

    let mut entries = tokio::fs::read_dir(&dir).await.map_err(|error| {
        AppError::InternalError(anyhow::anyhow!("Failed to read workflows dir: {error}"))
    })?;

    let mut commands = Vec::new();
    while let Some(entry) = entries.next_entry().await.map_err(|error| {
        AppError::InternalError(anyhow::anyhow!("Failed to read entry: {error}"))
    })? {
        let path = entry.path();
        if path.extension().and_then(|value| value.to_str()) != Some("md") {
            continue;
        }

        let name = path
            .file_stem()
            .and_then(|value| value.to_str())
            .unwrap_or_default()
            .to_string();
        if name.is_empty() {
            continue;
        }

        let metadata = entry.metadata().await.map_err(|error| {
            AppError::InternalError(anyhow::anyhow!("Failed to read metadata: {error}"))
        })?;
        let filename = path
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or_default()
            .to_string();

        commands.push(CommandItem {
            id: format!("workflow-{name}"),
            name: name.clone(),
            display_name: name.clone(),
            description: format!("Workflow: {name}"),
            command_type: "workflow".to_string(),
            category: None,
            tags: None,
            metadata: serde_json::json!({
                "filename": filename,
                "size": metadata.len(),
                "source": "global"
            }),
        });
    }

    Ok(commands)
}

pub(super) fn skill_to_command(skill: &SkillDefinition) -> CommandItem {
    CommandItem {
        id: format!("skill-{}", skill.id),
        name: skill.id.clone(),
        display_name: skill.name.clone(),
        description: skill.description.clone(),
        command_type: "skill".to_string(),
        category: None,
        tags: None,
        metadata: serde_json::json!({
            "prompt": skill.prompt,
            "toolRefs": skill.tool_refs,
            "license": skill.license,
            "compatibility": skill.compatibility,
            "metadata": skill.metadata,
        }),
    }
}

pub(super) async fn list_mcp_tools_as_commands(
    state: &AppState,
) -> Result<Vec<CommandItem>, AppError> {
    let aliases = state.mcp_manager.tool_index().all_aliases();
    let commands = aliases
        .into_iter()
        .filter_map(|alias| {
            state
                .mcp_manager
                .get_tool_info(&alias.server_id, &alias.original_name)
                .map(|tool| CommandItem {
                    id: format!("mcp-{}-{}", alias.server_id, alias.original_name),
                    name: alias.alias.clone(),
                    display_name: alias.alias.clone(),
                    description: tool.description.clone(),
                    command_type: "mcp".to_string(),
                    category: Some("MCP Tools".to_string()),
                    tags: None,
                    metadata: serde_json::json!({
                        "serverId": alias.server_id,
                        "originalName": alias.original_name,
                    }),
                })
        })
        .collect();

    Ok(commands)
}