Skip to main content

ai_agent/utils/plugins/
load_plugin_hooks.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/loadPluginHooks.ts
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5use std::sync::Mutex;
6
7use once_cell::sync::Lazy;
8use serde_json::Value;
9
10use super::loader::load_all_plugins_cache_only;
11
12static PLUGIN_HOOK_CACHE: Lazy<Mutex<Option<HashMap<String, Vec<PluginHookMatcher>>>>> =
13    Lazy::new(|| Mutex::new(None));
14
15/// Hook event types.
16pub const HOOK_EVENTS: &[&str] = &[
17    "PreToolUse",
18    "PostToolUse",
19    "PostToolUseFailure",
20    "PermissionDenied",
21    "Notification",
22    "UserPromptSubmit",
23    "SessionStart",
24    "SessionEnd",
25    "Stop",
26    "StopFailure",
27    "SubagentStart",
28    "SubagentStop",
29    "PreCompact",
30    "PostCompact",
31    "PermissionRequest",
32    "Setup",
33    "TeammateIdle",
34    "TaskCreated",
35    "TaskCompleted",
36    "Elicitation",
37    "ElicitationResult",
38    "ConfigChange",
39    "WorktreeCreate",
40    "WorktreeRemove",
41    "InstructionsLoaded",
42    "CwdChanged",
43    "FileChanged",
44];
45
46/// Plugin hook matcher with plugin context.
47#[derive(Clone, Debug)]
48pub struct PluginHookMatcher {
49    pub matcher: String,
50    pub hooks: Vec<String>,
51    pub plugin_root: String,
52    pub plugin_name: String,
53    pub plugin_id: String,
54}
55
56/// Convert plugin hooks configuration to native matchers with plugin context.
57fn convert_plugin_hooks_to_matchers(
58    plugin: &crate::plugin::types::LoadedPlugin,
59) -> HashMap<String, Vec<PluginHookMatcher>> {
60    let mut plugin_matchers: HashMap<String, Vec<PluginHookMatcher>> = HashMap::new();
61
62    for &event in HOOK_EVENTS {
63        plugin_matchers.insert(event.to_string(), Vec::new());
64    }
65
66    let hooks_config = match &plugin.hooks_config {
67        Some(config) => config,
68        None => return plugin_matchers,
69    };
70
71    if let Some(obj) = hooks_config.as_object() {
72        for (event, matchers) in obj {
73            if let Some(matchers_array) = matchers.as_array() {
74                for matcher_entry in matchers_array {
75                    if let Some(hooks) = matcher_entry.get("hooks").and_then(|h| h.as_array()) {
76                        if !hooks.is_empty() {
77                            let matcher = matcher_entry
78                                .get("matcher")
79                                .and_then(|m| m.as_str())
80                                .unwrap_or("*")
81                                .to_string();
82
83                            let hook_list: Vec<String> = hooks
84                                .iter()
85                                .filter_map(|h| h.as_str().map(|s| s.to_string()))
86                                .collect();
87
88                            let entry = plugin_matchers.entry(event.clone()).or_default();
89                            entry.push(PluginHookMatcher {
90                                matcher,
91                                hooks: hook_list,
92                                plugin_root: plugin.path.clone(),
93                                plugin_name: plugin.name.clone(),
94                                plugin_id: plugin.source.clone(),
95                            });
96                        }
97                    }
98                }
99            }
100        }
101    }
102
103    plugin_matchers
104}
105
106/// Load and register hooks from all enabled plugins.
107pub async fn load_plugin_hooks()
108-> Result<HashMap<String, Vec<PluginHookMatcher>>, Box<dyn std::error::Error + Send + Sync>> {
109    let plugin_result = load_all_plugins_cache_only().await?;
110    let mut all_plugin_hooks: HashMap<String, Vec<PluginHookMatcher>> = HashMap::new();
111
112    for &event in HOOK_EVENTS {
113        all_plugin_hooks.insert(event.to_string(), Vec::new());
114    }
115
116    for plugin in &plugin_result.enabled {
117        let plugin_matchers = convert_plugin_hooks_to_matchers(plugin);
118
119        for (event, matchers) in plugin_matchers {
120            all_plugin_hooks.entry(event).or_default().extend(matchers);
121        }
122    }
123
124    let total_hooks: usize = all_plugin_hooks
125        .values()
126        .map(|matchers| matchers.iter().map(|m| m.hooks.len()).sum::<usize>())
127        .sum();
128
129    log::debug!(
130        "Registered {} hooks from {} plugins",
131        total_hooks,
132        plugin_result.enabled.len()
133    );
134
135    {
136        let mut cache = PLUGIN_HOOK_CACHE.lock().unwrap();
137        *cache = Some(all_plugin_hooks.clone());
138    }
139
140    Ok(all_plugin_hooks)
141}
142
143/// Clear the plugin hook cache.
144pub fn clear_plugin_hook_cache() {
145    let mut cache = PLUGIN_HOOK_CACHE.lock().unwrap();
146    *cache = None;
147}
148
149/// Remove hooks from plugins no longer in the enabled set.
150pub async fn prune_removed_plugin_hooks() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
151    let plugin_result = load_all_plugins_cache_only().await?;
152    let enabled_roots: std::collections::HashSet<_> = plugin_result
153        .enabled
154        .iter()
155        .map(|p| p.path.clone())
156        .collect();
157
158    let cache = PLUGIN_HOOK_CACHE.lock().unwrap();
159    let current = match cache.as_ref() {
160        Some(c) => c.clone(),
161        None => return Ok(()),
162    };
163
164    let mut survivors: HashMap<String, Vec<PluginHookMatcher>> = HashMap::new();
165    for (event, matchers) in current {
166        let kept: Vec<PluginHookMatcher> = matchers
167            .into_iter()
168            .filter(|m| enabled_roots.contains(&m.plugin_root))
169            .collect();
170        if !kept.is_empty() {
171            survivors.insert(event, kept);
172        }
173    }
174
175    drop(cache);
176    {
177        let mut cache = PLUGIN_HOOK_CACHE.lock().unwrap();
178        *cache = Some(survivors);
179    }
180
181    Ok(())
182}
183
184/// Reset hot reload subscription state.
185pub fn reset_hot_reload_state() {
186    // Test-only function
187}