Skip to main content

ai_agent/utils/plugins/
load_plugin_output_styles.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/loadPluginOutputStyles.ts
2#![allow(dead_code)]
3
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::Mutex;
7
8use once_cell::sync::Lazy;
9
10use super::frontmatter_parser::parse_frontmatter;
11use super::loader::load_all_plugins_cache_only;
12use super::walk_plugin_markdown::{WalkPluginMarkdownOpts, walk_plugin_markdown};
13
14static OUTPUT_STYLE_CACHE: Lazy<Mutex<Option<Vec<OutputStyleConfig>>>> =
15    Lazy::new(|| Mutex::new(None));
16
17/// Output style configuration loaded from a plugin.
18#[derive(Clone, Debug)]
19pub struct OutputStyleConfig {
20    pub name: String,
21    pub description: String,
22    pub prompt: String,
23    pub source: String,
24    pub force_for_plugin: Option<bool>,
25}
26
27/// Load output styles from a directory.
28async fn load_output_styles_from_directory(
29    output_styles_path: &Path,
30    plugin_name: &str,
31    loaded_paths: &mut HashSet<String>,
32) -> Result<Vec<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
33    use std::sync::Arc;
34    use tokio::sync::Mutex;
35
36    let styles: Arc<Mutex<Vec<OutputStyleConfig>>> = Arc::new(Mutex::new(Vec::new()));
37
38    walk_plugin_markdown(
39        output_styles_path,
40        |full_path, _namespace| {
41            let plugin_name = plugin_name.to_string();
42            let styles = Arc::clone(&styles);
43
44            Box::pin(async move {
45                match load_output_style_from_file(&full_path, &plugin_name, &mut HashSet::new())
46                    .await
47                {
48                    Ok(Some(style)) => styles.lock().await.push(style),
49                    _ => {}
50                }
51            })
52        },
53        WalkPluginMarkdownOpts {
54            stop_at_skill_dir: Some(false),
55            log_label: Some("output-styles".to_string()),
56        },
57    )
58    .await
59    .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
60
61    Ok(Arc::try_unwrap(styles).unwrap().into_inner())
62}
63
64/// Load a single output style from a file.
65async fn load_output_style_from_file(
66    file_path: &str,
67    plugin_name: &str,
68    loaded_paths: &mut HashSet<String>,
69) -> Result<Option<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
70    if loaded_paths.contains(file_path) {
71        return Ok(None);
72    }
73    loaded_paths.insert(file_path.to_string());
74
75    let content = tokio::fs::read_to_string(file_path)
76        .await
77        .map_err(|e| format!("Failed to read {}: {}", file_path, e))?;
78    let (frontmatter, markdown_content) = parse_frontmatter(&content, file_path);
79
80    let file_name = std::path::Path::new(file_path)
81        .file_stem()
82        .map(|s| s.to_string_lossy().to_string())
83        .unwrap_or_default();
84
85    let base_style_name = frontmatter
86        .get("name")
87        .and_then(|v| v.as_str())
88        .unwrap_or(&file_name);
89
90    let name = format!("{}:{}", plugin_name, base_style_name);
91
92    let description = frontmatter
93        .get("description")
94        .and_then(|v| v.as_str())
95        .unwrap_or(&format!("Output style from {} plugin", plugin_name))
96        .to_string();
97
98    let force_for_plugin = frontmatter.get("force-for-plugin").and_then(|v| match v {
99        serde_json::Value::Bool(b) => Some(*b),
100        serde_json::Value::String(s) => match s.as_str() {
101            "true" => Some(true),
102            "false" => Some(false),
103            _ => None,
104        },
105        _ => None,
106    });
107
108    Ok(Some(OutputStyleConfig {
109        name,
110        description,
111        prompt: markdown_content.trim().to_string(),
112        source: "plugin".to_string(),
113        force_for_plugin,
114    }))
115}
116
117/// Load plugin output styles from all enabled plugins.
118pub async fn load_plugin_output_styles()
119-> Result<Vec<OutputStyleConfig>, Box<dyn std::error::Error + Send + Sync>> {
120    {
121        let cache = OUTPUT_STYLE_CACHE.lock().unwrap();
122        if let Some(ref styles) = *cache {
123            return Ok(styles.clone());
124        }
125    }
126
127    let plugin_result = load_all_plugins_cache_only().await?;
128    let mut all_styles = Vec::new();
129
130    for plugin in &plugin_result.enabled {
131        let mut loaded_paths = HashSet::new();
132
133        // Load from default output-styles directory
134        if let Some(ref output_styles_path) = plugin.output_styles_path {
135            match load_output_styles_from_directory(
136                Path::new(output_styles_path),
137                &plugin.name,
138                &mut loaded_paths,
139            )
140            .await
141            {
142                Ok(styles) => {
143                    log::debug!(
144                        "Loaded {} output styles from plugin {} default directory",
145                        styles.len(),
146                        plugin.name
147                    );
148                    all_styles.extend(styles);
149                }
150                Err(e) => log::debug!(
151                    "Failed to load output styles from plugin {} default directory: {}",
152                    plugin.name,
153                    e
154                ),
155            }
156        }
157
158        // Load from additional paths
159        if let Some(ref output_styles_paths) = plugin.output_styles_paths {
160            for style_path in output_styles_paths {
161                let metadata = match tokio::fs::metadata(style_path).await {
162                    Ok(m) => m,
163                    Err(_) => continue,
164                };
165
166                if metadata.is_dir() {
167                    if let Ok(styles) = load_output_styles_from_directory(
168                        Path::new(style_path),
169                        &plugin.name,
170                        &mut loaded_paths,
171                    )
172                    .await
173                    {
174                        all_styles.extend(styles);
175                    }
176                } else if metadata.is_file()
177                    && Path::new(style_path)
178                        .extension()
179                        .map(|e| e.to_string_lossy() == "md")
180                        .unwrap_or(false)
181                {
182                    if let Ok(Some(style)) =
183                        load_output_style_from_file(style_path, &plugin.name, &mut loaded_paths)
184                            .await
185                    {
186                        all_styles.push(style);
187                    }
188                }
189            }
190        }
191    }
192
193    log::debug!("Total plugin output styles loaded: {}", all_styles.len());
194
195    {
196        let mut cache = OUTPUT_STYLE_CACHE.lock().unwrap();
197        *cache = Some(all_styles.clone());
198    }
199
200    Ok(all_styles)
201}
202
203/// Clear the plugin output style cache.
204pub fn clear_plugin_output_style_cache() {
205    let mut cache = OUTPUT_STYLE_CACHE.lock().unwrap();
206    *cache = None;
207}