Skip to main content

ai_agent/utils/plugins/
plugin_options_storage.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/pluginOptionsStorage.ts
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5use std::sync::Mutex;
6
7use once_cell::sync::Lazy;
8
9use super::plugin_directories::get_plugin_data_dir;
10use crate::plugin::types::LoadedPlugin;
11
12pub type PluginOptionValues = HashMap<String, serde_json::Value>;
13pub type PluginOptionSchema = HashMap<String, serde_json::Value>;
14
15static PLUGIN_OPTIONS_CACHE: Lazy<Mutex<HashMap<String, PluginOptionValues>>> =
16    Lazy::new(|| Mutex::new(HashMap::new()));
17
18/// Get the canonical storage key for a plugin's options.
19pub fn get_plugin_storage_id(plugin: &LoadedPlugin) -> String {
20    plugin.source.clone()
21}
22
23/// Load saved option values for a plugin.
24pub fn load_plugin_options(plugin_id: &str) -> PluginOptionValues {
25    {
26        let cache = PLUGIN_OPTIONS_CACHE.lock().unwrap();
27        if let Some(values) = cache.get(plugin_id) {
28            return values.clone();
29        }
30    }
31
32    let merged = PluginOptionValues::new();
33
34    {
35        let mut cache = PLUGIN_OPTIONS_CACHE.lock().unwrap();
36        cache.insert(plugin_id.to_string(), merged.clone());
37    }
38
39    merged
40}
41
42/// Clear the plugin options cache.
43pub fn clear_plugin_options_cache() {
44    let mut cache = PLUGIN_OPTIONS_CACHE.lock().unwrap();
45    cache.clear();
46}
47
48/// Save option values.
49pub fn save_plugin_options(
50    _plugin_id: &str,
51    _values: &PluginOptionValues,
52    _schema: &PluginOptionSchema,
53) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
54    clear_plugin_options_cache();
55    Ok(())
56}
57
58/// Delete all stored option values for a plugin.
59pub fn delete_plugin_options(_plugin_id: &str) {
60    clear_plugin_options_cache();
61}
62
63/// Find option keys whose saved values don't satisfy the schema.
64pub fn get_unconfigured_options(_plugin: &LoadedPlugin) -> PluginOptionSchema {
65    // Stub: simplified implementation
66    HashMap::new()
67}
68
69/// Substitute ${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_DATA} with their paths.
70pub fn substitute_plugin_variables(value: &str, plugin_path: &str, source: &str) -> String {
71    let normalize = |p: &str| -> String {
72        if cfg!(windows) {
73            p.replace('\\', "/")
74        } else {
75            p.to_string()
76        }
77    };
78
79    let mut out = value.replace("${CLAUDE_PLUGIN_ROOT}", &normalize(plugin_path));
80
81    out = out.replace(
82        "${CLAUDE_PLUGIN_DATA}",
83        &normalize(&get_plugin_data_dir(source)),
84    );
85
86    out
87}
88
89/// Substitute ${user_config.KEY} with saved option values.
90pub fn substitute_user_config_variables(value: &str, user_config: &PluginOptionValues) -> String {
91    let mut result = value.to_string();
92    let re = regex::Regex::new(r"\$\{user_config\.([^}]+)\}").unwrap();
93
94    for cap in re.captures_iter(value) {
95        let key = &cap[1];
96        let full_match = &cap[0];
97
98        if let Some(config_value) = user_config.get(key) {
99            result = result.replace(full_match, &config_value.to_string());
100        } else {
101            log::debug!("Missing user config value for key: {}", key);
102        }
103    }
104
105    result
106}
107
108/// Content-safe variant for skill/agent prose.
109pub fn substitute_user_config_in_content(
110    content: &str,
111    options: &PluginOptionValues,
112    schema: &PluginOptionSchema,
113) -> String {
114    let re = regex::Regex::new(r"\$\{user_config\.([^}]+)\}").unwrap();
115
116    re.replace_all(content, |caps: &regex::Captures| {
117        let key = &caps[1];
118
119        if let Some(field_schema) = schema.get(key) {
120            let is_sensitive = field_schema
121                .get("sensitive")
122                .and_then(|s| s.as_bool())
123                .unwrap_or(false);
124
125            if is_sensitive {
126                return format!(
127                    "[sensitive option '{}' not available in skill content]",
128                    key
129                );
130            }
131        }
132
133        match options.get(key) {
134            Some(value) => value.to_string(),
135            None => caps[0].to_string(),
136        }
137    })
138    .to_string()
139}