terminal_jarvis/tools/
tools_config.rs

1// Tools Configuration Domain
2// Handles loading tool configurations from modular config files and user preferences
3
4use anyhow::{anyhow, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// Complete tool definition loaded from config files
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct ToolDefinition {
12    pub display_name: String,
13    pub config_key: String,
14    pub description: String,
15    pub homepage: String,
16    pub documentation: String,
17    pub cli_command: String,
18    pub requires_npm: bool,
19    pub requires_sudo: bool,
20    pub status: String,
21    pub install: InstallCommand,
22    pub update: InstallCommand,
23    pub auth: AuthDefinition,
24    pub features: Option<ToolFeatures>,
25}
26
27/// Installation/update command definition
28#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct InstallCommand {
30    pub command: String,
31    pub args: Vec<String>,
32    pub pipe_to: Option<String>, // For curl-based installations that pipe to bash
33    pub verify_command: Option<String>,
34    pub post_install_message: Option<String>,
35}
36
37/// Authentication configuration
38#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct AuthDefinition {
40    pub env_vars: Vec<String>,
41    pub setup_url: String,
42    pub browser_auth: bool,
43    pub auth_instructions: Option<String>,
44}
45
46/// Tool feature capabilities
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct ToolFeatures {
49    pub supports_files: bool,
50    pub supports_streaming: bool,
51    pub supports_conversation: bool,
52    pub max_context_tokens: Option<u64>,
53    pub supported_languages: Vec<String>,
54}
55
56/// User preferences for individual tools
57#[derive(Debug, Clone, Deserialize, Serialize)]
58pub struct ToolPreferences {
59    #[serde(default = "default_true")]
60    pub enabled: bool,
61    #[serde(default = "default_true")]
62    pub auto_update: bool,
63}
64
65fn default_true() -> bool {
66    true
67}
68
69/// Configuration loader for tools
70pub struct ToolConfigLoader {
71    /// All discovered tool definitions
72    tool_definitions: HashMap<String, ToolDefinition>,
73    /// User preferences from terminal-jarvis.toml
74    user_preferences: HashMap<String, ToolPreferences>,
75}
76
77impl Default for ToolConfigLoader {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl ToolConfigLoader {
84    /// Create new config loader and discover all tools
85    pub fn new() -> Self {
86        let mut loader = Self {
87            tool_definitions: HashMap::new(),
88            user_preferences: HashMap::new(),
89        };
90
91        // Auto-discover tools from config/tools/ directory
92        if let Err(e) = loader.load_builtin_tools() {
93            eprintln!("Warning: Failed to load tool configurations: {}", e);
94        }
95
96        // Load user preferences if available
97        if let Err(e) = loader.load_user_preferences() {
98            eprintln!("Warning: Failed to load user preferences: {}", e);
99        }
100
101        loader
102    }
103
104    /// Auto-discover and load tools from config/tools/ directory
105    fn load_builtin_tools(&mut self) -> Result<()> {
106        let config_dirs = vec![
107            std::env::current_exe()
108                .ok()
109                .and_then(|exe| exe.parent().map(|p| p.join("../config/tools"))),
110            Some(PathBuf::from("./config/tools")),
111            Some(PathBuf::from("../config/tools")),
112        ];
113
114        for config_dir in config_dirs.into_iter().flatten() {
115            if config_dir.exists() && config_dir.is_dir() {
116                if let Ok(entries) = std::fs::read_dir(&config_dir) {
117                    for entry in entries.flatten() {
118                        if let Some(file_name) = entry.file_name().to_str() {
119                            if file_name.ends_with(".toml") {
120                                let tool_name = file_name.trim_end_matches(".toml");
121                                if let Ok(tool_config) = self.load_tool_config(&entry.path()) {
122                                    self.tool_definitions
123                                        .insert(tool_name.to_string(), tool_config);
124                                }
125                            }
126                        }
127                    }
128                }
129                break; // Use the first config directory found
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Load individual tool configuration from TOML file
137    fn load_tool_config(&self, path: &PathBuf) -> Result<ToolDefinition> {
138        let content = std::fs::read_to_string(path)?;
139
140        // Parse the tool TOML file
141        #[derive(Deserialize)]
142        struct ToolFile {
143            tool: ToolDefinition,
144        }
145
146        let tool_file: ToolFile = toml::from_str(&content)
147            .map_err(|e| anyhow!("Failed to parse tool config {}: {}", path.display(), e))?;
148
149        Ok(tool_file.tool)
150    }
151
152    /// Load user preferences from terminal-jarvis.toml files
153    fn load_user_preferences(&mut self) -> Result<()> {
154        let config_paths = vec![
155            dirs::config_dir().map(|p| p.join("terminal-jarvis").join("config.toml")),
156            Some(PathBuf::from("./terminal-jarvis.toml")),
157        ];
158
159        for path in config_paths.into_iter().flatten() {
160            if path.exists() {
161                if let Ok(content) = std::fs::read_to_string(&path) {
162                    if let Ok(user_config) = toml::from_str::<UserConfigFile>(&content) {
163                        if let Some(prefs) = user_config.preferences {
164                            if let Some(tools) = prefs.tools {
165                                self.user_preferences.extend(tools);
166                            }
167                        }
168                        break; // Use the first config file found
169                    }
170                }
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Get tool definition by name
178    pub fn get_tool_definition(&self, tool_name: &str) -> Option<&ToolDefinition> {
179        self.tool_definitions.get(tool_name)
180    }
181
182    /// Get all tool names that have definitions
183    pub fn get_tool_names(&self) -> Vec<String> {
184        self.tool_definitions.keys().cloned().collect()
185    }
186
187    /// Check if tool is enabled by user preferences
188    #[allow(dead_code)] // Used for configuration management
189    pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
190        if let Some(tool_def) = self.tool_definitions.get(tool_name) {
191            if let Some(prefs) = self.user_preferences.get(&tool_def.config_key) {
192                return prefs.enabled;
193            }
194        }
195        true // Default to enabled
196    }
197
198    /// Get install command for tool
199    pub fn get_install_command(&self, tool_name: &str) -> Option<&InstallCommand> {
200        self.tool_definitions.get(tool_name).map(|t| &t.install)
201    }
202
203    /// Get update command for tool
204    #[allow(dead_code)] // Used for update functionality
205    pub fn get_update_command(&self, tool_name: &str) -> Option<&InstallCommand> {
206        self.tool_definitions.get(tool_name).map(|t| &t.update)
207    }
208
209    /// Get authentication info for tool
210    #[allow(dead_code)] // Used for auth management
211    pub fn get_auth_info(&self, tool_name: &str) -> Option<&AuthDefinition> {
212        self.tool_definitions.get(tool_name).map(|t| &t.auth)
213    }
214
215    /// Get display name to config key mapping (for compatibility)
216    #[allow(dead_code)] // Used for service mapping
217    pub fn get_display_name_to_config_mapping(&self) -> HashMap<String, String> {
218        self.tool_definitions
219            .values()
220            .map(|tool_def| (tool_def.display_name.clone(), tool_def.config_key.clone()))
221            .collect()
222    }
223
224    /// Check if tool requires sudo
225    #[allow(dead_code)] // Used for installation privilege checking
226    pub fn requires_sudo(&self, tool_name: &str) -> bool {
227        self.tool_definitions
228            .get(tool_name)
229            .map(|t| t.requires_sudo)
230            .unwrap_or(false)
231    }
232
233    /// Get tools that require NPM
234    #[allow(dead_code)] // Used for NPM dependency validation
235    pub fn get_npm_tools(&self) -> Vec<String> {
236        self.tool_definitions
237            .iter()
238            .filter(|(_, tool_def)| tool_def.requires_npm)
239            .map(|(name, _)| name.clone())
240            .collect()
241    }
242}
243
244/// Simplified user config file structure for reading preferences
245#[derive(Debug, Deserialize)]
246struct UserConfigFile {
247    preferences: Option<UserPreferencesFile>,
248}
249
250#[derive(Debug, Deserialize)]
251struct UserPreferencesFile {
252    tools: Option<HashMap<String, ToolPreferences>>,
253}
254
255/// Global tool config loader instance using safe singleton pattern
256static TOOL_CONFIG_LOADER: std::sync::OnceLock<ToolConfigLoader> = std::sync::OnceLock::new();
257
258/// Get global tool config loader (singleton pattern)
259pub fn get_tool_config_loader() -> &'static ToolConfigLoader {
260    TOOL_CONFIG_LOADER.get_or_init(ToolConfigLoader::new)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_tool_config_loader_creation() {
269        let loader = ToolConfigLoader::new();
270        // Should not panic and should be able to get tool names
271        let _tool_names = loader.get_tool_names();
272    }
273
274    #[test]
275    fn test_global_config_loader() {
276        let loader = get_tool_config_loader();
277        let _tool_names = loader.get_tool_names();
278    }
279}