use crate::event::{EventKind, Wire};
use serde_json::{json, Map, Value};
use std::fmt;
use std::io::Write;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OutputError {
Unsupported { event: String, output: &'static str },
}
impl fmt::Display for OutputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported { event, output } => {
write!(f, "{event} does not take {output}")
}
}
}
}
impl std::error::Error for OutputError {}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Reply {
pub stdout: Option<Value>,
pub stderr: Option<String>,
pub exit_code: i32,
}
impl Reply {
pub fn none() -> Self {
Self::default()
}
pub fn is_none(&self) -> bool {
self.stdout.is_none() && self.stderr.is_none() && self.exit_code == 0
}
pub fn additional_context(kind: &EventKind, context: &str) -> Result<Self, OutputError> {
if !kind.takes_additional_context() {
return Err(OutputError::Unsupported {
event: kind.to_string(),
output: "additionalContext",
});
}
Ok(Self {
stdout: Some(json!({
"hookSpecificOutput": {
"hookEventName": kind.as_str(),
"additionalContext": context,
}
})),
..Self::default()
})
}
pub fn block_stop(reason: &str) -> Self {
Self {
stdout: Some(json!({"decision": "block", "reason": reason})),
..Self::default()
}
}
pub fn deny_tool(wire: Wire, reason: &str) -> Self {
Self::refuse_tool(wire, reason, "deny")
}
pub fn ask_tool(wire: Wire, reason: &str) -> Self {
Self::refuse_tool(wire, reason, "ask")
}
fn refuse_tool(wire: Wire, reason: &str, decision: &str) -> Self {
match wire {
Wire::Claude => Self {
stdout: Some(json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": decision,
"permissionDecisionReason": reason,
}
})),
..Self::default()
},
Wire::Codex => Self {
stdout: None,
stderr: Some(reason.to_owned()),
exit_code: 2,
},
}
}
pub fn with_system_message(mut self, message: &str) -> Self {
if self.stderr.is_some() {
return self;
}
let object = match self.stdout.take() {
Some(Value::Object(object)) => object,
_ => Map::new(),
};
let mut object = object;
object.insert("systemMessage".into(), Value::String(message.to_owned()));
self.stdout = Some(Value::Object(object));
self
}
pub fn stdout_line(&self) -> Option<String> {
self.stdout
.as_ref()
.and_then(|value| serde_json::to_string(value).ok())
}
pub fn write_to(&self, mut out: impl Write, mut err: impl Write) -> i32 {
if let Some(text) = &self.stderr {
let _ = writeln!(err, "{text}");
}
if let Some(line) = self.stdout_line() {
let _ = writeln!(out, "{line}");
}
self.exit_code
}
}