use serde_json::{Map, Value};
pub mod claude_code;
pub mod cline;
pub mod codex_cli;
#[derive(Debug, Clone, Copy)]
pub struct HookOutput<'a> {
pub decision: &'a str,
pub reason: Option<&'a str>,
pub context: Option<&'a VerdictContext<'a>>,
}
impl HookOutput<'_> {
pub const fn allow() -> Self {
Self {
decision: "allow",
reason: None,
context: None,
}
}
}
pub type Verdict<'a> = HookOutput<'a>;
#[derive(Debug, Clone, Copy)]
pub struct VerdictContext<'a> {
pub headline: &'a str,
pub body: &'a str,
}
pub fn translate(agent: &str, event: &str, verdict: &Verdict<'_>) -> Value {
match agent {
"claude-code" => claude_code::translate(event, verdict),
"cline" => cline::translate(event, verdict),
"codex-cli" => codex_cli::translate(event, verdict),
_ => empty(),
}
}
pub fn translate_delivery(
agent: &str,
event: &str,
verdict: &Verdict<'_>,
updated_input: Option<&serde_json::Map<String, Value>>,
additional_context: Option<&str>,
system_message: Option<&str>,
defer: bool,
) -> Value {
match agent {
"claude-code" => claude_code::translate_delivery(
event,
verdict,
updated_input,
additional_context,
system_message,
defer,
),
"cline" => cline::translate_delivery(event, verdict),
"codex-cli" => {
codex_cli::translate_delivery(event, verdict, updated_input, system_message, defer)
}
_ => empty(),
}
}
#[inline]
pub fn empty() -> Value {
Value::Object(Map::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_agent_yields_empty_object() {
let out = translate("meta-llama-agent", "pre_tool_use", &Verdict::allow());
assert_eq!(out, empty());
}
#[test]
fn delivery_dispatch_knows_cline() {
let denied = translate_delivery(
"cline",
"pre_tool_use",
&Verdict {
decision: "block",
reason: Some("credential detected"),
context: None,
},
None,
None,
None,
false,
);
assert_eq!(
denied,
serde_json::json!({ "skip": true, "reason": "credential detected" }),
"the live hook path dropped a Cline deny"
);
let updated = Map::from_iter([("command".to_string(), serde_json::json!("safe"))]);
assert_eq!(
translate_delivery(
"cline",
"pre_tool_use",
&Verdict::allow(),
Some(&updated),
Some("an alert is queued"),
Some("a note"),
true,
),
empty()
);
}
#[test]
fn empty_is_literally_empty_object() {
let s = serde_json::to_string(&empty()).unwrap();
assert_eq!(s, "{}");
}
}