Skip to main content

polyhook_core/
events.rs

1use crate::types::CallerKind;
2
3/// Normalize a vendor-specific event name to the canonical polyhook event name.
4///
5/// If no mapping exists the original name is returned unchanged.
6pub fn normalize_event(vendor: &str, caller: &CallerKind) -> String {
7    let canonical = match caller {
8        CallerKind::ClaudeCode => normalize_claude_code_event(vendor),
9        CallerKind::Cursor => normalize_cursor_event(vendor),
10        CallerKind::Windsurf => normalize_windsurf_event(vendor),
11        CallerKind::Cline => normalize_cline_event(vendor),
12        CallerKind::Amp => normalize_amp_event(vendor),
13        CallerKind::Unknown => None,
14    };
15
16    canonical
17        .map(|s| s.to_owned())
18        .unwrap_or_else(|| vendor.to_owned())
19}
20
21fn normalize_claude_code_event(vendor: &str) -> Option<&'static str> {
22    match vendor {
23        "PreToolUse" => Some("tool:before"),
24        "PostToolUse" => Some("tool:after"),
25        "Startup" => Some("session:start"),
26        "Stop" => Some("session:stop"),
27        "SubagentStop" => Some("agent:stop"),
28        "Notification" => Some("notification"),
29        _ => None,
30    }
31}
32
33fn normalize_cursor_event(vendor: &str) -> Option<&'static str> {
34    match vendor {
35        "BeforeToolCall" => Some("tool:before"),
36        "AfterToolCall" => Some("tool:after"),
37        "SessionStart" => Some("session:start"),
38        "SessionEnd" => Some("session:stop"),
39        "Notification" => Some("notification"),
40        _ => None,
41    }
42}
43
44fn normalize_windsurf_event(vendor: &str) -> Option<&'static str> {
45    match vendor {
46        "pre_tool" => Some("tool:before"),
47        "post_tool" => Some("tool:after"),
48        "session_start" => Some("session:start"),
49        "session_end" => Some("session:stop"),
50        "notification" => Some("notification"),
51        _ => None,
52    }
53}
54
55fn normalize_cline_event(vendor: &str) -> Option<&'static str> {
56    match vendor {
57        "beforeToolUse" => Some("tool:before"),
58        "afterToolUse" => Some("tool:after"),
59        "onStart" => Some("session:start"),
60        "onStop" => Some("session:stop"),
61        _ => None,
62    }
63}
64
65fn normalize_amp_event(vendor: &str) -> Option<&'static str> {
66    match vendor {
67        "tool.before" => Some("tool:before"),
68        "tool.after" => Some("tool:after"),
69        "session.start" => Some("session:start"),
70        "session.stop" => Some("session:stop"),
71        "agent.stop" => Some("agent:stop"),
72        _ => None,
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Tests
78// ---------------------------------------------------------------------------
79
80#[cfg(test)]
81mod tests {
82    use super::normalize_event;
83    use crate::CallerKind;
84
85    #[test]
86    fn unknown_caller_returns_original_string() {
87        assert_eq!(normalize_event("SomeVendorEvent", &CallerKind::Unknown), "SomeVendorEvent");
88        assert_eq!(normalize_event("PreToolUse", &CallerKind::Unknown), "PreToolUse");
89        assert_eq!(normalize_event("", &CallerKind::Unknown), "");
90    }
91
92    // ClaudeCode — all arms
93    #[test] fn cc_startup()      { assert_eq!(normalize_event("Startup",      &CallerKind::ClaudeCode), "session:start"); }
94    #[test] fn cc_stop()         { assert_eq!(normalize_event("Stop",         &CallerKind::ClaudeCode), "session:stop"); }
95    #[test] fn cc_subagent_stop(){ assert_eq!(normalize_event("SubagentStop", &CallerKind::ClaudeCode), "agent:stop"); }
96    #[test] fn cc_notification() { assert_eq!(normalize_event("Notification", &CallerKind::ClaudeCode), "notification"); }
97    #[test] fn cc_unknown_falls_through() { assert_eq!(normalize_event("Bogus", &CallerKind::ClaudeCode), "Bogus"); }
98
99    // Cursor — all arms
100    #[test] fn cur_session_start() { assert_eq!(normalize_event("SessionStart", &CallerKind::Cursor), "session:start"); }
101    #[test] fn cur_session_end()   { assert_eq!(normalize_event("SessionEnd",   &CallerKind::Cursor), "session:stop"); }
102    #[test] fn cur_notification()  { assert_eq!(normalize_event("Notification", &CallerKind::Cursor), "notification"); }
103    #[test] fn cur_unknown_falls_through() { assert_eq!(normalize_event("Bogus", &CallerKind::Cursor), "Bogus"); }
104
105    // Windsurf — all arms
106    #[test] fn ws_session_start() { assert_eq!(normalize_event("session_start", &CallerKind::Windsurf), "session:start"); }
107    #[test] fn ws_session_end()   { assert_eq!(normalize_event("session_end",   &CallerKind::Windsurf), "session:stop"); }
108    #[test] fn ws_notification()  { assert_eq!(normalize_event("notification",  &CallerKind::Windsurf), "notification"); }
109    #[test] fn ws_unknown_falls_through() { assert_eq!(normalize_event("bogus", &CallerKind::Windsurf), "bogus"); }
110
111    // Cline — all arms
112    #[test] fn cl_on_start() { assert_eq!(normalize_event("onStart", &CallerKind::Cline), "session:start"); }
113    #[test] fn cl_on_stop()  { assert_eq!(normalize_event("onStop",  &CallerKind::Cline), "session:stop"); }
114    #[test] fn cl_unknown_falls_through() { assert_eq!(normalize_event("bogus", &CallerKind::Cline), "bogus"); }
115
116    // Amp — all arms
117    #[test] fn amp_session_start() { assert_eq!(normalize_event("session.start", &CallerKind::Amp), "session:start"); }
118    #[test] fn amp_session_stop()  { assert_eq!(normalize_event("session.stop",  &CallerKind::Amp), "session:stop"); }
119    #[test] fn amp_agent_stop()    { assert_eq!(normalize_event("agent.stop",    &CallerKind::Amp), "agent:stop"); }
120    #[test] fn amp_unknown_falls_through() { assert_eq!(normalize_event("bogus.bogus", &CallerKind::Amp), "bogus.bogus"); }
121}