Skip to main content

agent_code_lib/services/
plugins.rs

1//! Plugin system.
2//!
3//! Plugins bundle skills, commands, and configuration together as
4//! installable packages. A plugin is a directory containing:
5//!
6//! - `plugin.toml` — metadata and configuration
7//! - `skills/` — skill files to register
8//! - `hooks/` — hook definitions
9//!
10//! Plugins are loaded from `~/.config/agent-code/plugins/` and
11//! project-level `.agent/plugins/`.
12
13use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use tracing::{debug, warn};
16
17/// Plugin metadata from plugin.toml.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PluginManifest {
20    pub name: String,
21    pub version: Option<String>,
22    pub description: Option<String>,
23    pub author: Option<String>,
24    /// Skills provided by this plugin.
25    #[serde(default)]
26    pub skills: Vec<String>,
27    /// Hook definitions.
28    #[serde(default)]
29    pub hooks: Vec<PluginHook>,
30    /// Configuration keys this plugin adds.
31    #[serde(default)]
32    pub config: std::collections::HashMap<String, serde_json::Value>,
33}
34
35/// A hook defined by a plugin.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PluginHook {
38    pub event: String,
39    pub command: String,
40    pub tool_name: Option<String>,
41}
42
43/// A loaded plugin with its manifest and path.
44#[derive(Debug, Clone)]
45pub struct Plugin {
46    pub manifest: PluginManifest,
47    pub path: PathBuf,
48}
49
50/// Registry of loaded plugins.
51pub struct PluginRegistry {
52    plugins: Vec<Plugin>,
53}
54
55impl PluginRegistry {
56    pub fn new() -> Self {
57        Self {
58            plugins: Vec::new(),
59        }
60    }
61
62    /// Load plugins from all configured directories.
63    pub fn load_all(project_root: Option<&Path>) -> Self {
64        let mut registry = Self::new();
65
66        // User-level plugins.
67        if let Some(dir) = user_plugin_dir() {
68            registry.load_from_dir(&dir);
69        }
70
71        // Project-level plugins.
72        if let Some(root) = project_root {
73            registry.load_from_dir(&root.join(".agent").join("plugins"));
74        }
75
76        debug!("Loaded {} plugins", registry.plugins.len());
77        registry
78    }
79
80    fn load_from_dir(&mut self, dir: &Path) {
81        if !dir.is_dir() {
82            return;
83        }
84
85        let entries = match std::fs::read_dir(dir) {
86            Ok(e) => e,
87            Err(_) => return,
88        };
89
90        for entry in entries.flatten() {
91            let path = entry.path();
92            if !path.is_dir() {
93                continue;
94            }
95
96            let manifest_path = path.join("plugin.toml");
97            if !manifest_path.exists() {
98                continue;
99            }
100
101            match load_plugin(&path) {
102                Ok(plugin) => {
103                    debug!(
104                        "Loaded plugin '{}' from {}",
105                        plugin.manifest.name,
106                        path.display()
107                    );
108                    self.plugins.push(plugin);
109                }
110                Err(e) => {
111                    warn!("Failed to load plugin at {}: {e}", path.display());
112                }
113            }
114        }
115    }
116
117    /// Get all loaded plugins.
118    pub fn all(&self) -> &[Plugin] {
119        &self.plugins
120    }
121
122    /// Find a plugin by name.
123    pub fn find(&self, name: &str) -> Option<&Plugin> {
124        self.plugins.iter().find(|p| p.manifest.name == name)
125    }
126
127    /// Get all skill directories from loaded plugins.
128    pub fn skill_dirs(&self) -> Vec<PathBuf> {
129        self.plugins
130            .iter()
131            .map(|p| p.path.join("skills"))
132            .filter(|d| d.is_dir())
133            .collect()
134    }
135
136    /// Get all hook definitions from loaded plugins.
137    pub fn hooks(&self) -> Vec<&PluginHook> {
138        self.plugins
139            .iter()
140            .flat_map(|p| &p.manifest.hooks)
141            .collect()
142    }
143
144    /// Discover all executable tools from plugin bin/ directories.
145    pub fn executable_tools(&self) -> Vec<crate::tools::plugin_exec::PluginExecTool> {
146        self.plugins
147            .iter()
148            .flat_map(|p| {
149                crate::tools::plugin_exec::discover_plugin_executables(&p.path, &p.manifest.name)
150            })
151            .collect()
152    }
153}
154
155fn load_plugin(path: &Path) -> Result<Plugin, String> {
156    let manifest_path = path.join("plugin.toml");
157    let content =
158        std::fs::read_to_string(&manifest_path).map_err(|e| format!("Read error: {e}"))?;
159
160    let manifest: PluginManifest =
161        toml::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
162
163    Ok(Plugin {
164        manifest,
165        path: path.to_path_buf(),
166    })
167}
168
169fn user_plugin_dir() -> Option<PathBuf> {
170    dirs::config_dir().map(|d| d.join("agent-code").join("plugins"))
171}