1use crate::event::{EventKind, Wire};
18use serde_json::{json, Map, Value};
19use std::fmt;
20use std::io::Write;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum OutputError {
24 Unsupported { event: String, output: &'static str },
26}
27
28impl fmt::Display for OutputError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Unsupported { event, output } => {
32 write!(f, "{event} does not take {output}")
33 }
34 }
35 }
36}
37
38impl std::error::Error for OutputError {}
39
40#[derive(Clone, Debug, PartialEq, Default)]
42pub struct Reply {
43 pub stdout: Option<Value>,
44 pub stderr: Option<String>,
45 pub exit_code: i32,
46}
47
48impl Reply {
49 pub fn none() -> Self {
51 Self::default()
52 }
53
54 pub fn is_none(&self) -> bool {
55 self.stdout.is_none() && self.stderr.is_none() && self.exit_code == 0
56 }
57
58 pub fn additional_context(kind: &EventKind, context: &str) -> Result<Self, OutputError> {
60 if !kind.takes_additional_context() {
61 return Err(OutputError::Unsupported {
62 event: kind.to_string(),
63 output: "additionalContext",
64 });
65 }
66 Ok(Self {
67 stdout: Some(json!({
68 "hookSpecificOutput": {
69 "hookEventName": kind.as_str(),
70 "additionalContext": context,
71 }
72 })),
73 ..Self::default()
74 })
75 }
76
77 pub fn block_stop(reason: &str) -> Self {
79 Self {
80 stdout: Some(json!({"decision": "block", "reason": reason})),
81 ..Self::default()
82 }
83 }
84
85 pub fn deny_tool(wire: Wire, reason: &str) -> Self {
87 Self::refuse_tool(wire, reason, "deny")
88 }
89
90 pub fn ask_tool(wire: Wire, reason: &str) -> Self {
93 Self::refuse_tool(wire, reason, "ask")
94 }
95
96 fn refuse_tool(wire: Wire, reason: &str, decision: &str) -> Self {
97 match wire {
98 Wire::Claude => Self {
99 stdout: Some(json!({
100 "hookSpecificOutput": {
101 "hookEventName": "PreToolUse",
102 "permissionDecision": decision,
103 "permissionDecisionReason": reason,
104 }
105 })),
106 ..Self::default()
107 },
108 Wire::Codex => Self {
109 stdout: None,
110 stderr: Some(reason.to_owned()),
111 exit_code: 2,
112 },
113 }
114 }
115
116 pub fn with_system_message(mut self, message: &str) -> Self {
118 if self.stderr.is_some() {
119 return self;
120 }
121 let object = match self.stdout.take() {
122 Some(Value::Object(object)) => object,
123 _ => Map::new(),
124 };
125 let mut object = object;
126 object.insert("systemMessage".into(), Value::String(message.to_owned()));
127 self.stdout = Some(Value::Object(object));
128 self
129 }
130
131 pub fn stdout_line(&self) -> Option<String> {
133 self.stdout
134 .as_ref()
135 .and_then(|value| serde_json::to_string(value).ok())
136 }
137
138 pub fn write_to(&self, mut out: impl Write, mut err: impl Write) -> i32 {
141 if let Some(text) = &self.stderr {
142 let _ = writeln!(err, "{text}");
143 }
144 if let Some(line) = self.stdout_line() {
145 let _ = writeln!(out, "{line}");
146 }
147 self.exit_code
148 }
149}