Skip to main content

hermes_agent_cli_core/
tools.rs

1use anyhow::{Context, Result};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs;
6use std::path::PathBuf;
7
8/// Built-in tools available in Hermes
9#[derive(Debug, Clone)]
10pub struct Tool {
11    pub name: &'static str,
12    pub description: &'static str,
13    pub toolset: &'static str,
14    pub enabled_by_default: bool,
15}
16
17/// List of all built-in tools
18pub fn get_builtin_tools() -> Vec<Tool> {
19    vec![
20        Tool {
21            name: "web_search",
22            description: "Search the web for information",
23            toolset: "web",
24            enabled_by_default: true,
25        },
26        Tool {
27            name: "web_fetch",
28            description: "Fetch content from a URL",
29            toolset: "web",
30            enabled_by_default: true,
31        },
32        Tool {
33            name: "file_read",
34            description: "Read files from the filesystem",
35            toolset: "terminal",
36            enabled_by_default: true,
37        },
38        Tool {
39            name: "file_write",
40            description: "Write files to the filesystem",
41            toolset: "terminal",
42            enabled_by_default: true,
43        },
44        Tool {
45            name: "bash",
46            description: "Execute bash commands",
47            toolset: "terminal",
48            enabled_by_default: false,
49        },
50        Tool {
51            name: "powershell",
52            description: "Execute PowerShell commands",
53            toolset: "terminal",
54            enabled_by_default: false,
55        },
56        Tool {
57            name: "browser",
58            description: "Control a web browser",
59            toolset: "browser",
60            enabled_by_default: false,
61        },
62        Tool {
63            name: "github",
64            description: "Interact with GitHub API",
65            toolset: "github",
66            enabled_by_default: false,
67        },
68        Tool {
69            name: "jira",
70            description: "Interact with Jira",
71            toolset: "jira",
72            enabled_by_default: false,
73        },
74        Tool {
75            name: "database",
76            description: "Execute database queries",
77            toolset: "database",
78            enabled_by_default: false,
79        },
80        Tool {
81            name: "mcp",
82            description: "Use MCP (Model Context Protocol) tools",
83            toolset: "mcp",
84            enabled_by_default: false,
85        },
86    ]
87}
88
89/// Tools configuration storage
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct ToolsConfig {
92    #[serde(default)]
93    pub disabled: HashSet<String>,
94}
95
96impl ToolsConfig {
97    /// Load tools config
98    pub fn load() -> Result<Self> {
99        let path = Self::tools_config_path();
100        if !path.exists() {
101            return Ok(ToolsConfig::default());
102        }
103        let content = fs::read_to_string(&path)
104            .with_context(|| format!("failed to read tools config from {:?}", path))?;
105        let config: ToolsConfig = serde_yaml::from_str(&content)
106            .with_context(|| format!("failed to parse tools config from {:?}", path))?;
107        Ok(config)
108    }
109
110    /// Save tools config
111    pub fn save(&self) -> Result<()> {
112        let path = Self::tools_config_path();
113        if let Some(parent) = path.parent() {
114            fs::create_dir_all(parent)
115                .with_context(|| format!("failed to create tools config directory {:?}", parent))?;
116        }
117        let content = serde_yaml::to_string(self).context("failed to serialize tools config")?;
118        fs::write(&path, content)
119            .with_context(|| format!("failed to write tools config to {:?}", path))?;
120        Ok(())
121    }
122
123    /// Get tools config path
124    fn tools_config_path() -> PathBuf {
125        if let Ok(home) = std::env::var("HERMES_HOME") {
126            return PathBuf::from(home).join("tools.yaml");
127        }
128        if let Ok(profile) = std::env::var("HERMES_PROFILE") {
129            if let Some(proj_dirs) =
130                ProjectDirs::from("ai", "hermes", &format!("hermes-{}", profile))
131            {
132                return proj_dirs.config_dir().join("tools.yaml");
133            }
134        }
135        if let Some(proj_dirs) = ProjectDirs::from("ai", "hermes", "hermes-cli") {
136            return proj_dirs.config_dir().join("tools.yaml");
137        }
138        if let Ok(home) = std::env::var("USERPROFILE") {
139            return PathBuf::from(home).join(".hermes").join("tools.yaml");
140        }
141        PathBuf::from(".hermes").join("tools.yaml")
142    }
143
144    /// Check if a tool is disabled
145    pub fn is_disabled(&self, tool_name: &str) -> bool {
146        self.disabled.contains(tool_name)
147    }
148
149    /// Disable a tool
150    pub fn disable(&mut self, tool_name: &str) -> bool {
151        self.disabled.insert(tool_name.to_string())
152    }
153
154    /// Enable a tool
155    pub fn enable(&mut self, tool_name: &str) -> bool {
156        self.disabled.remove(tool_name)
157    }
158}
159
160/// List all tools with their status
161pub fn list_tools(all: bool) -> Result<Vec<(String, String, String, bool)>> {
162    let config =
163        ToolsConfig::load().map_err(|e| anyhow::anyhow!("failed to load tools config: {}", e))?;
164    let tools = get_builtin_tools();
165
166    Ok(tools
167        .into_iter()
168        .filter(|tool| all || !config.is_disabled(tool.name))
169        .map(|tool| {
170            let enabled = !config.is_disabled(tool.name);
171            (tool.name.to_string(), tool.description.to_string(), tool.toolset.to_string(), enabled)
172        })
173        .collect())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_tools_config_disable_enable() {
182        let mut config = ToolsConfig::default();
183        assert!(!config.is_disabled("web_search"));
184        config.disable("web_search");
185        assert!(config.is_disabled("web_search"));
186        config.enable("web_search");
187        assert!(!config.is_disabled("web_search"));
188    }
189
190    #[test]
191    fn test_get_builtin_tools() {
192        let tools = get_builtin_tools();
193        assert!(!tools.is_empty());
194        assert!(tools.iter().any(|t| t.name == "web_search"));
195        assert!(tools.iter().any(|t| t.name == "file_read"));
196    }
197}