Skip to main content

ai_agent/plugin/
builtin_plugins.rs

1// Source: ~/claudecode/openclaudecode/src/plugins/builtinPlugins.ts
2//! Built-in Plugin Registry
3//!
4//! Manages built-in plugins that ship with the CLI and can be enabled/disabled
5//! by users via the /plugin UI.
6
7use std::collections::{HashMap, HashSet};
8use std::sync::Mutex;
9
10use crate::plugin::types::{LoadedPlugin, PluginManifest};
11use crate::types::plugin::BuiltinPluginDefinition;
12
13const BUILTIN_MARKETPLACE_NAME: &str = "builtin";
14
15static BUILTIN_PLUGINS: Mutex<Vec<BuiltinPluginDefinition>> = Mutex::new(Vec::new());
16
17/// The marketplace name for built-in plugins.
18pub const BUILTIN_MARKETPLACE_NAME_CONST: &str = BUILTIN_MARKETPLACE_NAME;
19
20/// Register a built-in plugin. Call this from init or at startup.
21pub fn register_builtin_plugin(definition: BuiltinPluginDefinition) {
22    let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
23    plugins.push(definition);
24}
25
26/// Check if a plugin ID represents a built-in plugin (ends with @builtin).
27pub fn is_builtin_plugin_id(plugin_id: &str) -> bool {
28    plugin_id.ends_with(&format!("@{}", BUILTIN_MARKETPLACE_NAME))
29}
30
31/// Get a specific built-in plugin definition by name.
32/// Returns `None` if not found. Since the definition contains closures,
33/// we return a clone of the clonable fields instead.
34pub fn get_builtin_plugin_definition(name: &str) -> Option<BuiltinPluginSummary> {
35    let plugins = BUILTIN_PLUGINS.lock().unwrap();
36    plugins
37        .iter()
38        .find(|p| p.name == name)
39        .map(|d| BuiltinPluginSummary {
40            name: d.name.clone(),
41            description: d.description.clone(),
42            version: d.version.clone(),
43            has_skills: d.skills.is_some(),
44            has_hooks: d.hooks.is_some(),
45            has_mcp_servers: d.mcp_servers.is_some(),
46            default_enabled: d.default_enabled,
47        })
48}
49
50/// Summary of a built-in plugin definition (no closures).
51#[derive(Debug, Clone)]
52pub struct BuiltinPluginSummary {
53    pub name: String,
54    pub description: String,
55    pub version: Option<String>,
56    pub has_skills: bool,
57    pub has_hooks: bool,
58    pub has_mcp_servers: bool,
59    pub default_enabled: Option<bool>,
60}
61
62/// Result of getting built-in plugins, split by enabled/disabled state.
63#[derive(Debug, Default)]
64pub struct BuiltinPluginResult {
65    pub enabled: Vec<LoadedPlugin>,
66    pub disabled: Vec<LoadedPlugin>,
67}
68
69/// Get all registered built-in plugins as LoadedPlugin objects, split into
70/// enabled/disabled based on user settings (with defaultEnabled as fallback).
71/// Plugins whose isAvailable() returns false are omitted entirely.
72pub fn get_builtin_plugins() -> BuiltinPluginResult {
73    let plugins = BUILTIN_PLUGINS.lock().unwrap();
74    let mut enabled = Vec::new();
75    let mut disabled = Vec::new();
76
77    // Load user-scope enabled plugins settings
78    let user_enabled_plugins = load_user_enabled_plugins();
79
80    for definition in plugins.iter() {
81        // Skip plugins that are not available
82        if let Some(is_avail) = &definition.is_available {
83            if !is_avail() {
84                continue;
85            }
86        }
87
88        let plugin_id = format!("{}@{}", definition.name, BUILTIN_MARKETPLACE_NAME);
89        let user_setting = user_enabled_plugins.get(&plugin_id);
90
91        // Enabled state: user preference > plugin default > true
92        let is_enabled = match user_setting {
93            Some(&true) => true,
94            Some(&false) => false,
95            None => definition.default_enabled.unwrap_or(true),
96        };
97
98        let plugin = LoadedPlugin {
99            name: definition.name.clone(),
100            manifest: PluginManifest {
101                name: definition.name.clone(),
102                version: definition.version.clone(),
103                description: Some(definition.description.clone()),
104                author: None,
105                homepage: None,
106                repository: None,
107                license: None,
108                keywords: None,
109                dependencies: None,
110                commands: None,
111                agents: None,
112                skills: None,
113                hooks: None,
114                output_styles: None,
115                channels: None,
116                mcp_servers: None,
117                lsp_servers: None,
118                settings: None,
119                user_config: None,
120            },
121            path: BUILTIN_MARKETPLACE_NAME.to_string(),
122            source: plugin_id.clone(),
123            repository: plugin_id,
124            enabled: Some(is_enabled),
125            is_builtin: Some(true),
126            sha: None,
127            commands_path: None,
128            commands_paths: None,
129            commands_metadata: None,
130            agents_path: None,
131            agents_paths: None,
132            skills_path: None,
133            skills_paths: None,
134            output_styles_path: None,
135            output_styles_paths: None,
136            hooks_config: definition.hooks.clone(),
137            mcp_servers: definition.mcp_servers.clone(),
138            lsp_servers: None,
139            settings: None,
140        };
141
142        if is_enabled {
143            enabled.push(plugin);
144        } else {
145            disabled.push(plugin);
146        }
147    }
148
149    BuiltinPluginResult { enabled, disabled }
150}
151
152/// Load the user's enabled plugins settings from the settings file.
153/// Returns a map of plugin_id -> enabled_state.
154fn load_user_enabled_plugins() -> HashMap<String, bool> {
155    let settings_dir = match std::env::var("AI_CODE_CONFIG_HOME") {
156        Ok(dir) => dir,
157        Err(_) => {
158            let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
159            format!("{}/.ai", home)
160        }
161    };
162
163    let settings_path = format!("{}/settings.json", settings_dir);
164    let content = match std::fs::read_to_string(&settings_path) {
165        Ok(c) => c,
166        Err(_) => return HashMap::new(),
167    };
168
169    let settings: serde_json::Value = match serde_json::from_str(&content) {
170        Ok(v) => v,
171        Err(_) => return HashMap::new(),
172    };
173
174    let mut result = HashMap::new();
175    if let Some(enabled_plugins) = settings.get("enabledPlugins").and_then(|v| v.as_object()) {
176        for (plugin_id, enabled) in enabled_plugins {
177            if let Some(val) = enabled.as_bool() {
178                result.insert(plugin_id.clone(), val);
179            }
180        }
181    }
182
183    result
184}
185
186/// Get skills from enabled built-in plugins as BundledSkillDefinitions.
187/// Skills from disabled plugins are not returned.
188/// Returns the names of enabled built-in plugins that have skills defined.
189pub fn get_builtin_plugin_skill_definitions() -> Vec<String> {
190    let BuiltinPluginResult { enabled, .. } = get_builtin_plugins();
191
192    // Collect enabled plugin names that have skills defined
193    let enabled_names: HashSet<&str> = enabled.iter().map(|p| p.name.as_str()).collect();
194
195    let plugins = BUILTIN_PLUGINS.lock().unwrap();
196    plugins
197        .iter()
198        .filter(|d| enabled_names.contains(d.name.as_str()) && d.skills.is_some())
199        .map(|d| d.name.clone())
200        .collect()
201}
202
203/// Clear built-in plugins registry (for testing).
204pub fn clear_builtin_plugins() {
205    let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
206    plugins.clear();
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_register_and_get_builtin_plugin() {
215        clear_builtin_plugins();
216
217        let definition = BuiltinPluginDefinition {
218            name: "test-plugin".to_string(),
219            description: "A test built-in plugin".to_string(),
220            version: Some("1.0.0".to_string()),
221            skills: None,
222            hooks: None,
223            mcp_servers: None,
224            is_available: None,
225            default_enabled: Some(true),
226        };
227
228        register_builtin_plugin(definition);
229
230        let result = get_builtin_plugin_definition("test-plugin");
231        assert!(result.is_some());
232        assert_eq!(result.unwrap().description, "A test built-in plugin");
233
234        clear_builtin_plugins();
235    }
236
237    #[test]
238    fn test_is_builtin_plugin_id() {
239        assert!(is_builtin_plugin_id("my-plugin@builtin"));
240        assert!(!is_builtin_plugin_id("my-plugin@marketplace"));
241        assert!(!is_builtin_plugin_id("my-plugin"));
242    }
243
244    #[test]
245    fn test_get_builtin_plugins_enabled_disabled() {
246        clear_builtin_plugins();
247
248        let enabled_plugin = BuiltinPluginDefinition {
249            name: "enabled-plugin".to_string(),
250            description: "Should be enabled".to_string(),
251            version: None,
252            skills: None,
253            hooks: None,
254            mcp_servers: None,
255            is_available: None,
256            default_enabled: Some(true),
257        };
258
259        let disabled_plugin = BuiltinPluginDefinition {
260            name: "disabled-plugin".to_string(),
261            description: "Should be disabled".to_string(),
262            version: None,
263            skills: None,
264            hooks: None,
265            mcp_servers: None,
266            is_available: None,
267            default_enabled: Some(false),
268        };
269
270        register_builtin_plugin(enabled_plugin);
271        register_builtin_plugin(disabled_plugin);
272
273        let result = get_builtin_plugins();
274        assert_eq!(result.enabled.len(), 1);
275        assert_eq!(result.disabled.len(), 1);
276        assert_eq!(result.enabled[0].name, "enabled-plugin");
277        assert_eq!(result.disabled[0].name, "disabled-plugin");
278
279        clear_builtin_plugins();
280    }
281
282    #[test]
283    fn test_get_builtin_plugins_filters_unavailable() {
284        clear_builtin_plugins();
285
286        let unavailable = BuiltinPluginDefinition {
287            name: "unavailable-plugin".to_string(),
288            description: "Should be filtered".to_string(),
289            version: None,
290            skills: None,
291            hooks: None,
292            mcp_servers: None,
293            is_available: Some(Box::new(|| false)),
294            default_enabled: Some(true),
295        };
296
297        let available = BuiltinPluginDefinition {
298            name: "available-plugin".to_string(),
299            description: "Should be included".to_string(),
300            version: None,
301            skills: None,
302            hooks: None,
303            mcp_servers: None,
304            is_available: Some(Box::new(|| true)),
305            default_enabled: Some(true),
306        };
307
308        register_builtin_plugin(unavailable);
309        register_builtin_plugin(available);
310
311        let result = get_builtin_plugins();
312        assert_eq!(result.enabled.len(), 1);
313        assert_eq!(result.enabled[0].name, "available-plugin");
314
315        clear_builtin_plugins();
316    }
317
318    #[test]
319    fn test_clear_builtin_plugins() {
320        clear_builtin_plugins();
321
322        let definition = BuiltinPluginDefinition {
323            name: "to-clear".to_string(),
324            description: "Will be cleared".to_string(),
325            version: None,
326            skills: None,
327            hooks: None,
328            mcp_servers: None,
329            is_available: None,
330            default_enabled: None,
331        };
332
333        register_builtin_plugin(definition);
334        assert!(get_builtin_plugin_definition("to-clear").is_some());
335
336        clear_builtin_plugins();
337        assert!(get_builtin_plugin_definition("to-clear").is_none());
338    }
339}