codex_hooks/
declarations.rs1use codex_plugin::PluginHookSource;
2use codex_protocol::protocol::HookEventName;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PluginHookDeclaration {
7 pub key: String,
8 pub event_name: HookEventName,
9}
10
11pub fn plugin_hook_declarations(hook_sources: &[PluginHookSource]) -> Vec<PluginHookDeclaration> {
13 let mut declarations = Vec::new();
14
15 for source in hook_sources {
16 let key_source = plugin_hook_key_source(
17 source.plugin_id.as_key().as_str(),
18 source.source_relative_path.as_str(),
19 );
20 for (event_name, groups) in source.hooks.clone().into_matcher_groups() {
21 for (group_index, group) in groups.iter().enumerate() {
22 for (handler_index, _) in group.hooks.iter().enumerate() {
23 declarations.push(PluginHookDeclaration {
24 key: crate::hook_key(&key_source, event_name, group_index, handler_index),
25 event_name,
26 });
27 }
28 }
29 }
30 }
31
32 declarations
33}
34
35pub(crate) fn plugin_hook_key_source(plugin_id: &str, source_relative_path: &str) -> String {
36 format!("{plugin_id}:{source_relative_path}")
37}
38
39#[cfg(test)]
40mod tests {
41 use codex_config::HookEventsToml;
42 use codex_config::HookHandlerConfig;
43 use codex_config::MatcherGroup;
44 use codex_plugin::PluginId;
45 use codex_utils_absolute_path::test_support::PathBufExt;
46 use codex_utils_absolute_path::test_support::test_path_buf;
47 use pretty_assertions::assert_eq;
48
49 use super::*;
50
51 #[test]
52 fn lists_declared_plugin_handlers_with_persisted_hook_keys() {
53 let plugin_root = test_path_buf("/tmp/plugin").abs();
54 let source_path = plugin_root.join("hooks/hooks.json");
55 let declarations = plugin_hook_declarations(&[PluginHookSource {
56 plugin_id: PluginId::parse("demo@test").expect("plugin id"),
57 plugin_root: plugin_root.clone(),
58 plugin_data_root: plugin_root.join("data"),
59 source_path,
60 source_relative_path: "hooks/hooks.json".to_string(),
61 hooks: HookEventsToml {
62 pre_tool_use: vec![MatcherGroup {
63 matcher: None,
64 hooks: vec![
65 HookHandlerConfig::Prompt {},
66 HookHandlerConfig::Command {
67 command: "echo hi".to_string(),
68 command_windows: None,
69 timeout_sec: None,
70 r#async: false,
71 status_message: None,
72 additional_context_limit: None,
73 },
74 ],
75 }],
76 session_start: vec![MatcherGroup {
77 matcher: None,
78 hooks: vec![HookHandlerConfig::Agent {}],
79 }],
80 ..Default::default()
81 },
82 }]);
83
84 assert_eq!(
85 declarations,
86 vec![
87 PluginHookDeclaration {
88 key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
89 event_name: HookEventName::PreToolUse,
90 },
91 PluginHookDeclaration {
92 key: "demo@test:hooks/hooks.json:pre_tool_use:0:1".to_string(),
93 event_name: HookEventName::PreToolUse,
94 },
95 PluginHookDeclaration {
96 key: "demo@test:hooks/hooks.json:session_start:0:0".to_string(),
97 event_name: HookEventName::SessionStart,
98 },
99 ]
100 );
101 }
102}