1use anyhow::{Context, Result};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs;
6use std::path::PathBuf;
7
8#[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
17pub 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91pub struct ToolsConfig {
92 #[serde(default)]
93 pub disabled: HashSet<String>,
94}
95
96impl ToolsConfig {
97 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 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 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 pub fn is_disabled(&self, tool_name: &str) -> bool {
146 self.disabled.contains(tool_name)
147 }
148
149 pub fn disable(&mut self, tool_name: &str) -> bool {
151 self.disabled.insert(tool_name.to_string())
152 }
153
154 pub fn enable(&mut self, tool_name: &str) -> bool {
156 self.disabled.remove(tool_name)
157 }
158}
159
160pub 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}