bamboo-tools 2026.5.1

Tool execution and integrations for the Bamboo agent framework
Documentation
use async_trait::async_trait;
use bamboo_agent_core::{Tool, ToolError, ToolExecutionContext, ToolResult};
use serde::Deserialize;
use serde_json::json;

use super::workspace_state;

#[derive(Debug, Deserialize)]
struct SlashCommandArgs {
    command: String,
}

pub struct SlashCommandTool;

impl SlashCommandTool {
    pub fn new() -> Self {
        Self
    }
}

impl Default for SlashCommandTool {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl Tool for SlashCommandTool {
    fn name(&self) -> &str {
        "SlashCommand"
    }

    fn description(&self) -> &str {
        "Execute a slash command within the main conversation"
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "Slash command text, including arguments"
                }
            },
            "required": ["command"],
            "additionalProperties": false
        })
    }

    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
        self.execute_with_context(args, ToolExecutionContext::none("SlashCommand"))
            .await
    }

    async fn execute_with_context(
        &self,
        args: serde_json::Value,
        ctx: ToolExecutionContext<'_>,
    ) -> Result<ToolResult, ToolError> {
        let parsed: SlashCommandArgs = serde_json::from_value(args).map_err(|e| {
            ToolError::InvalidArguments(format!("Invalid SlashCommand args: {}", e))
        })?;

        let raw = parsed.command.trim();
        if raw.is_empty() {
            return Err(ToolError::InvalidArguments(
                "command cannot be empty".to_string(),
            ));
        }

        let mut parts = raw.split_whitespace();
        let head = parts.next().unwrap_or_default();
        let tail = parts.collect::<Vec<_>>().join(" ");

        let project_path = Some(bamboo_infrastructure::paths::path_to_display_string(
            &workspace_state::workspace_or_process_cwd(ctx.session_id),
        ));

        let commands = crate::slash_commands::slash_commands_list(project_path)
            .await
            .map_err(ToolError::Execution)?;

        if let Some(command) = commands
            .into_iter()
            .find(|value| value.full_command == head)
        {
            let resolved = if command.accepts_arguments {
                command.content.replace("$ARGUMENTS", tail.trim())
            } else {
                command.content.clone()
            };

            return Ok(ToolResult {
                success: true,
                result: json!({
                    "command": raw,
                    "resolved_command": command.full_command,
                    "content": resolved,
                })
                .to_string(),
                display_preference: Some("Collapsible".to_string()),
            });
        }

        let fallback = crate::slash_commands::slash_commands_list(None)
            .await
            .unwrap_or_default();
        let available = fallback
            .iter()
            .take(5)
            .map(|value| value.full_command.clone())
            .collect::<Vec<_>>();

        Err(ToolError::Execution(format!(
            "Slash command '{}' not found. Available commands: {}",
            head,
            available.join(", ")
        )))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn slash_command_uses_session_workspace_for_project_commands() {
        let dir = tempfile::tempdir().unwrap();
        let commands_dir = dir.path().join(".claude/commands");
        tokio::fs::create_dir_all(&commands_dir).await.unwrap();
        tokio::fs::write(commands_dir.join("hello.md"), "Hi $ARGUMENTS")
            .await
            .unwrap();

        let session = format!("session_{}", uuid::Uuid::new_v4());
        super::workspace_state::set_workspace(&session, dir.path().to_path_buf());

        let tool = SlashCommandTool::new();
        let result = tool
            .execute_with_context(
                json!({ "command": "/hello world" }),
                ToolExecutionContext {
                    session_id: Some(&session),
                    tool_call_id: "call_1",
                    event_tx: None,
                    available_tool_schemas: None,
                },
            )
            .await
            .unwrap();

        let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
        assert_eq!(payload["resolved_command"], "/hello");
        assert_eq!(payload["content"], "Hi world");
    }
}