Skip to main content

vtcode_core/plugins/
components.rs

1//! Plugin component handlers for VT Code
2//!
3//! This module handles the different types of components that plugins can provide:
4//! - Commands (slash commands)
5//! - Agents (specialized profiles)
6//! - Skills (model-invoked capabilities)
7//! - Hooks (event handlers)
8//! - MCP servers (Model Context Protocol)
9
10use std::path::{Path, PathBuf};
11
12use anyhow::Result;
13use tokio::fs;
14
15use crate::plugins::PluginManifest;
16
17/// Handler for plugin commands (slash commands)
18pub struct CommandsHandler;
19
20impl CommandsHandler {
21    /// Process plugin commands from the plugin directory
22    pub async fn process_commands(
23        plugin_path: &Path,
24        manifest_commands: Option<Vec<String>>,
25    ) -> Result<Vec<PathBuf>> {
26        let mut command_files = Vec::new();
27
28        // Add commands from manifest paths
29        if let Some(manifest_paths) = manifest_commands {
30            for path in manifest_paths {
31                let full_path = plugin_path.join(&path);
32                if full_path.exists() && full_path.is_file() {
33                    command_files.push(full_path);
34                }
35            }
36        }
37
38        // Also look for commands in the default commands/ directory
39        let default_commands_dir = plugin_path.join("commands");
40        if default_commands_dir.exists() && default_commands_dir.is_dir() {
41            let mut entries = fs::read_dir(&default_commands_dir).await?;
42            while let Some(entry) = entries.next_entry().await? {
43                let path = entry.path();
44                if path.is_file() && path.extension().is_some_and(|ext| ext == "md") {
45                    command_files.push(path);
46                }
47            }
48        }
49
50        Ok(command_files)
51    }
52}
53
54/// Handler for plugin agents.
55pub struct AgentsHandler;
56
57impl AgentsHandler {
58    /// Process plugin agents from the plugin directory
59    pub async fn process_agents(
60        plugin_path: &Path,
61        manifest_agents: Option<Vec<String>>,
62    ) -> Result<Vec<PathBuf>> {
63        let mut agent_files = Vec::new();
64
65        // Add agents from manifest paths
66        if let Some(manifest_paths) = manifest_agents {
67            for path in manifest_paths {
68                let full_path = plugin_path.join(&path);
69                if full_path.exists() && full_path.is_file() {
70                    agent_files.push(full_path);
71                }
72            }
73        }
74
75        // Also look for agents in the default agents/ directory
76        let default_agents_dir = plugin_path.join("agents");
77        if default_agents_dir.exists() && default_agents_dir.is_dir() {
78            let mut entries = fs::read_dir(&default_agents_dir).await?;
79            while let Some(entry) = entries.next_entry().await? {
80                let path = entry.path();
81                if path.is_file() && path.extension().is_some_and(|ext| ext == "md") {
82                    agent_files.push(path);
83                }
84            }
85        }
86
87        Ok(agent_files)
88    }
89}
90
91/// Handler for plugin skills
92pub struct SkillsHandler;
93
94impl SkillsHandler {
95    /// Process plugin skills from the plugin directory
96    pub async fn process_skills(
97        plugin_path: &Path,
98        manifest_skills: Option<Vec<String>>,
99    ) -> Result<Vec<PathBuf>> {
100        let mut skill_dirs = Vec::new();
101
102        // Add skills from manifest paths
103        if let Some(manifest_paths) = manifest_skills {
104            for path in manifest_paths {
105                let full_path = plugin_path.join(&path);
106                if full_path.exists() && full_path.is_dir() {
107                    skill_dirs.push(full_path);
108                }
109            }
110        }
111
112        // Also look for skills in the default skills/ directory
113        let default_skills_dir = plugin_path.join("skills");
114        if default_skills_dir.exists() && default_skills_dir.is_dir() {
115            let mut entries = fs::read_dir(&default_skills_dir).await?;
116            while let Some(entry) = entries.next_entry().await? {
117                let path = entry.path();
118                if path.is_dir() {
119                    // Check if it contains a SKILL.md file
120                    let skill_md = path.join("SKILL.md");
121                    if skill_md.exists() && skill_md.is_file() {
122                        skill_dirs.push(path);
123                    }
124                }
125            }
126        }
127
128        Ok(skill_dirs)
129    }
130}
131
132/// Handler for plugin hooks
133pub struct HooksHandler;
134
135impl HooksHandler {
136    /// Process plugin hooks from the plugin directory
137    pub async fn process_hooks(
138        plugin_path: &Path,
139        manifest_hooks: Option<serde_json::Value>,
140    ) -> Result<Option<PathBuf>> {
141        // Check for hooks in manifest
142        if let Some(hooks_config) = manifest_hooks {
143            // If hooks config is a string, treat it as a path
144            if let Some(path_str) = hooks_config.as_str() {
145                let hooks_path = plugin_path.join(path_str);
146                if hooks_path.exists() && hooks_path.is_file() {
147                    return Ok(Some(hooks_path));
148                }
149            }
150        }
151
152        // Look for hooks in the default hooks/ directory
153        let default_hooks_path = plugin_path.join("hooks/hooks.json");
154        if default_hooks_path.exists() && default_hooks_path.is_file() {
155            return Ok(Some(default_hooks_path));
156        }
157
158        Ok(None)
159    }
160}
161
162/// Handler for plugin MCP servers
163pub struct McpServersHandler;
164
165impl McpServersHandler {
166    /// Process plugin MCP servers from the plugin directory
167    pub async fn process_mcp_servers(
168        plugin_path: &Path,
169        manifest_mcp: Option<serde_json::Value>,
170    ) -> Result<Option<PathBuf>> {
171        // Check for MCP config in manifest
172        if let Some(mcp_config) = manifest_mcp {
173            // If MCP config is a string, treat it as a path
174            if let Some(path_str) = mcp_config.as_str() {
175                let mcp_path = plugin_path.join(path_str);
176                if mcp_path.exists() && mcp_path.is_file() {
177                    return Ok(Some(mcp_path));
178                }
179            }
180        }
181
182        // Look for MCP config in the default .mcp.json file
183        let default_mcp_path = plugin_path.join(".mcp.json");
184        if default_mcp_path.exists() && default_mcp_path.is_file() {
185            return Ok(Some(default_mcp_path));
186        }
187
188        Ok(None)
189    }
190}
191
192/// Handler for plugin LSP servers
193pub struct LspServersHandler;
194
195impl LspServersHandler {
196    /// Process plugin LSP servers from the plugin directory
197    pub async fn process_lsp_servers(
198        plugin_path: &Path,
199        manifest_lsp: Option<serde_json::Value>,
200    ) -> Result<Option<PathBuf>> {
201        // Check for LSP config in manifest
202        if let Some(lsp_config) = manifest_lsp {
203            // If LSP config is a string, treat it as a path
204            if let Some(path_str) = lsp_config.as_str() {
205                let lsp_path = plugin_path.join(path_str);
206                if lsp_path.exists() && lsp_path.is_file() {
207                    return Ok(Some(lsp_path));
208                }
209            }
210        }
211
212        // Look for LSP config in the default .lsp.json file
213        let default_lsp_path = plugin_path.join(".lsp.json");
214        if default_lsp_path.exists() && default_lsp_path.is_file() {
215            return Ok(Some(default_lsp_path));
216        }
217
218        Ok(None)
219    }
220}
221
222/// A comprehensive handler that processes all plugin components
223pub struct PluginComponentsHandler;
224
225impl PluginComponentsHandler {
226    /// Process all components for a plugin
227    pub async fn process_all_components<P: AsRef<Path>>(
228        plugin_path: P,
229        manifest: &PluginManifest,
230    ) -> Result<PluginComponents> {
231        let path_buf = plugin_path.as_ref().to_path_buf();
232        let commands =
233            CommandsHandler::process_commands(&path_buf, manifest.commands.clone()).await?;
234
235        let agents = AgentsHandler::process_agents(&path_buf, manifest.agents.clone()).await?;
236
237        let skills = SkillsHandler::process_skills(&path_buf, manifest.skills.clone()).await?;
238
239        let hooks = HooksHandler::process_hooks(
240            &path_buf,
241            manifest.hooks.as_ref().map(|h| match h {
242                crate::plugins::manifest::HookConfig::Path(path) => {
243                    serde_json::Value::String(path.clone())
244                }
245                crate::plugins::manifest::HookConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
246            }),
247        )
248        .await?;
249
250        let mcp_servers = McpServersHandler::process_mcp_servers(
251            &path_buf,
252            manifest.mcp_servers.as_ref().map(|m| match m {
253                crate::plugins::manifest::McpServerConfig::Path(path) => {
254                    serde_json::Value::String(path.clone())
255                }
256                crate::plugins::manifest::McpServerConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
257            }),
258        )
259        .await?;
260
261        let lsp_servers = LspServersHandler::process_lsp_servers(
262            &path_buf,
263            manifest.lsp_servers.as_ref().map(|l| match l {
264                crate::plugins::manifest::LspServerConfig::Path(path) => {
265                    serde_json::Value::String(path.clone())
266                }
267                crate::plugins::manifest::LspServerConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
268            }),
269        )
270        .await?;
271
272        Ok(PluginComponents {
273            commands,
274            agents,
275            skills,
276            hooks,
277            mcp_servers,
278            lsp_servers,
279        })
280    }
281}
282
283/// Structure containing all plugin components
284pub struct PluginComponents {
285    pub commands: Vec<PathBuf>,
286    pub agents: Vec<PathBuf>,
287    pub skills: Vec<PathBuf>,
288    pub hooks: Option<PathBuf>,
289    pub mcp_servers: Option<PathBuf>,
290    pub lsp_servers: Option<PathBuf>,
291}