agent_code_lib/services/
plugins.rs1use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use tracing::{debug, warn};
16
17#[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 #[serde(default)]
26 pub skills: Vec<String>,
27 #[serde(default)]
29 pub hooks: Vec<PluginHook>,
30 #[serde(default)]
32 pub config: std::collections::HashMap<String, serde_json::Value>,
33}
34
35#[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#[derive(Debug, Clone)]
45pub struct Plugin {
46 pub manifest: PluginManifest,
47 pub path: PathBuf,
48}
49
50pub 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 pub fn load_all(project_root: Option<&Path>) -> Self {
64 let mut registry = Self::new();
65
66 if let Some(dir) = user_plugin_dir() {
68 registry.load_from_dir(&dir);
69 }
70
71 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 pub fn all(&self) -> &[Plugin] {
119 &self.plugins
120 }
121
122 pub fn find(&self, name: &str) -> Option<&Plugin> {
124 self.plugins.iter().find(|p| p.manifest.name == name)
125 }
126
127 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 pub fn hooks(&self) -> Vec<&PluginHook> {
138 self.plugins
139 .iter()
140 .flat_map(|p| &p.manifest.hooks)
141 .collect()
142 }
143
144 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}