polyhook-core 0.1.1

Core types and utilities for polyhook — write AI coding agent hooks once, run them everywhere
Documentation
use crate::types::CallerKind;

/// Normalize a vendor-specific event name to the canonical polyhook event name.
///
/// If no mapping exists the original name is returned unchanged.
pub fn normalize_event(vendor: &str, caller: &CallerKind) -> String {
    let canonical = match caller {
        CallerKind::ClaudeCode => normalize_claude_code_event(vendor),
        CallerKind::Cursor => normalize_cursor_event(vendor),
        CallerKind::Windsurf => normalize_windsurf_event(vendor),
        CallerKind::Cline => normalize_cline_event(vendor),
        CallerKind::Amp => normalize_amp_event(vendor),
        CallerKind::Unknown => None,
    };

    canonical
        .map(|s| s.to_owned())
        .unwrap_or_else(|| vendor.to_owned())
}

fn normalize_claude_code_event(vendor: &str) -> Option<&'static str> {
    match vendor {
        "PreToolUse" => Some("tool:before"),
        "PostToolUse" => Some("tool:after"),
        "Startup" => Some("session:start"),
        "Stop" => Some("session:stop"),
        "SubagentStop" => Some("agent:stop"),
        "Notification" => Some("notification"),
        _ => None,
    }
}

fn normalize_cursor_event(vendor: &str) -> Option<&'static str> {
    match vendor {
        "BeforeToolCall" => Some("tool:before"),
        "AfterToolCall" => Some("tool:after"),
        "SessionStart" => Some("session:start"),
        "SessionEnd" => Some("session:stop"),
        "Notification" => Some("notification"),
        _ => None,
    }
}

fn normalize_windsurf_event(vendor: &str) -> Option<&'static str> {
    match vendor {
        "pre_tool" => Some("tool:before"),
        "post_tool" => Some("tool:after"),
        "session_start" => Some("session:start"),
        "session_end" => Some("session:stop"),
        "notification" => Some("notification"),
        _ => None,
    }
}

fn normalize_cline_event(vendor: &str) -> Option<&'static str> {
    match vendor {
        "beforeToolUse" => Some("tool:before"),
        "afterToolUse" => Some("tool:after"),
        "onStart" => Some("session:start"),
        "onStop" => Some("session:stop"),
        _ => None,
    }
}

fn normalize_amp_event(vendor: &str) -> Option<&'static str> {
    match vendor {
        "tool.before" => Some("tool:before"),
        "tool.after" => Some("tool:after"),
        "session.start" => Some("session:start"),
        "session.stop" => Some("session:stop"),
        "agent.stop" => Some("agent:stop"),
        _ => None,
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::normalize_event;
    use crate::CallerKind;

    #[test]
    fn unknown_caller_returns_original_string() {
        assert_eq!(normalize_event("SomeVendorEvent", &CallerKind::Unknown), "SomeVendorEvent");
        assert_eq!(normalize_event("PreToolUse", &CallerKind::Unknown), "PreToolUse");
        assert_eq!(normalize_event("", &CallerKind::Unknown), "");
    }

    // ClaudeCode — all arms
    #[test] fn cc_startup()      { assert_eq!(normalize_event("Startup",      &CallerKind::ClaudeCode), "session:start"); }
    #[test] fn cc_stop()         { assert_eq!(normalize_event("Stop",         &CallerKind::ClaudeCode), "session:stop"); }
    #[test] fn cc_subagent_stop(){ assert_eq!(normalize_event("SubagentStop", &CallerKind::ClaudeCode), "agent:stop"); }
    #[test] fn cc_notification() { assert_eq!(normalize_event("Notification", &CallerKind::ClaudeCode), "notification"); }
    #[test] fn cc_unknown_falls_through() { assert_eq!(normalize_event("Bogus", &CallerKind::ClaudeCode), "Bogus"); }

    // Cursor — all arms
    #[test] fn cur_session_start() { assert_eq!(normalize_event("SessionStart", &CallerKind::Cursor), "session:start"); }
    #[test] fn cur_session_end()   { assert_eq!(normalize_event("SessionEnd",   &CallerKind::Cursor), "session:stop"); }
    #[test] fn cur_notification()  { assert_eq!(normalize_event("Notification", &CallerKind::Cursor), "notification"); }
    #[test] fn cur_unknown_falls_through() { assert_eq!(normalize_event("Bogus", &CallerKind::Cursor), "Bogus"); }

    // Windsurf — all arms
    #[test] fn ws_session_start() { assert_eq!(normalize_event("session_start", &CallerKind::Windsurf), "session:start"); }
    #[test] fn ws_session_end()   { assert_eq!(normalize_event("session_end",   &CallerKind::Windsurf), "session:stop"); }
    #[test] fn ws_notification()  { assert_eq!(normalize_event("notification",  &CallerKind::Windsurf), "notification"); }
    #[test] fn ws_unknown_falls_through() { assert_eq!(normalize_event("bogus", &CallerKind::Windsurf), "bogus"); }

    // Cline — all arms
    #[test] fn cl_on_start() { assert_eq!(normalize_event("onStart", &CallerKind::Cline), "session:start"); }
    #[test] fn cl_on_stop()  { assert_eq!(normalize_event("onStop",  &CallerKind::Cline), "session:stop"); }
    #[test] fn cl_unknown_falls_through() { assert_eq!(normalize_event("bogus", &CallerKind::Cline), "bogus"); }

    // Amp — all arms
    #[test] fn amp_session_start() { assert_eq!(normalize_event("session.start", &CallerKind::Amp), "session:start"); }
    #[test] fn amp_session_stop()  { assert_eq!(normalize_event("session.stop",  &CallerKind::Amp), "session:stop"); }
    #[test] fn amp_agent_stop()    { assert_eq!(normalize_event("agent.stop",    &CallerKind::Amp), "agent:stop"); }
    #[test] fn amp_unknown_falls_through() { assert_eq!(normalize_event("bogus.bogus", &CallerKind::Amp), "bogus.bogus"); }
}