1use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40 S: Serializer,
41{
42 match uuid {
43 Some(id) => serializer.serialize_str(&id.to_string()),
44 None => serializer.serialize_none(),
45 }
46}
47
48fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51 D: Deserializer<'de>,
52{
53 let opt_str: Option<String> = Option::deserialize(deserializer)?;
54 match opt_str {
55 Some(s) => Uuid::parse_str(&s)
56 .map(Some)
57 .map_err(serde::de::Error::custom),
58 None => Ok(None),
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66 User(UserMessage),
68
69 ControlRequest(ControlRequest),
71
72 ControlResponse(ControlResponse),
74
75 #[serde(untagged)]
77 Raw(Value),
78}
79
80#[derive(Debug, Clone)]
82pub struct ParseError {
83 pub raw_json: Value,
85 pub error_message: String,
87}
88
89impl fmt::Display for ParseError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
92 }
93}
94
95impl std::error::Error for ParseError {}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum ClaudeOutput {
101 System(SystemMessage),
103
104 User(UserMessage),
106
107 Assistant(AssistantMessage),
109
110 Result(ResultMessage),
112
113 ControlRequest(ControlRequest),
115
116 ControlResponse(ControlResponse),
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UserMessage {
123 pub message: MessageContent,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 #[serde(
126 serialize_with = "serialize_optional_uuid",
127 deserialize_with = "deserialize_optional_uuid"
128 )]
129 pub session_id: Option<Uuid>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MessageContent {
135 pub role: String,
136 #[serde(deserialize_with = "deserialize_content_blocks")]
137 pub content: Vec<ContentBlock>,
138}
139
140fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
142where
143 D: Deserializer<'de>,
144{
145 let value: Value = Value::deserialize(deserializer)?;
146 match value {
147 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
148 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
149 _ => Err(serde::de::Error::custom(
150 "content must be a string or array",
151 )),
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SystemMessage {
158 pub subtype: String,
159 #[serde(flatten)]
160 pub data: Value, }
162
163impl SystemMessage {
164 pub fn is_init(&self) -> bool {
166 self.subtype == "init"
167 }
168
169 pub fn is_status(&self) -> bool {
171 self.subtype == "status"
172 }
173
174 pub fn is_compact_boundary(&self) -> bool {
176 self.subtype == "compact_boundary"
177 }
178
179 pub fn as_init(&self) -> Option<InitMessage> {
181 if self.subtype != "init" {
182 return None;
183 }
184 serde_json::from_value(self.data.clone()).ok()
185 }
186
187 pub fn as_status(&self) -> Option<StatusMessage> {
189 if self.subtype != "status" {
190 return None;
191 }
192 serde_json::from_value(self.data.clone()).ok()
193 }
194
195 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
197 if self.subtype != "compact_boundary" {
198 return None;
199 }
200 serde_json::from_value(self.data.clone()).ok()
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InitMessage {
207 pub session_id: String,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub cwd: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub model: Option<String>,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub tools: Vec<String>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub mcp_servers: Vec<Value>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StatusMessage {
226 pub session_id: String,
228 pub status: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub uuid: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct CompactBoundaryMessage {
238 pub session_id: String,
240 pub compact_metadata: CompactMetadata,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub uuid: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactMetadata {
250 pub pre_tokens: u64,
252 pub trigger: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct AssistantMessage {
259 pub message: AssistantMessageContent,
260 pub session_id: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub uuid: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub parent_tool_use_id: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AssistantMessageContent {
270 pub id: String,
271 pub role: String,
272 pub model: String,
273 pub content: Vec<ContentBlock>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub stop_reason: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub stop_sequence: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub usage: Option<AssistantUsage>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AssistantUsage {
285 #[serde(default)]
287 pub input_tokens: u32,
288
289 #[serde(default)]
291 pub output_tokens: u32,
292
293 #[serde(default)]
295 pub cache_creation_input_tokens: u32,
296
297 #[serde(default)]
299 pub cache_read_input_tokens: u32,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub service_tier: Option<String>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub cache_creation: Option<CacheCreationDetails>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct CacheCreationDetails {
313 #[serde(default)]
315 pub ephemeral_1h_input_tokens: u32,
316
317 #[serde(default)]
319 pub ephemeral_5m_input_tokens: u32,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "type", rename_all = "snake_case")]
325pub enum ContentBlock {
326 Text(TextBlock),
327 Image(ImageBlock),
328 Thinking(ThinkingBlock),
329 ToolUse(ToolUseBlock),
330 ToolResult(ToolResultBlock),
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TextBlock {
336 pub text: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ImageBlock {
342 pub source: ImageSource,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ImageSource {
348 #[serde(rename = "type")]
349 pub source_type: String, pub media_type: String, pub data: String, }
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ThinkingBlock {
357 pub thinking: String,
358 pub signature: String,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ToolUseBlock {
364 pub id: String,
365 pub name: String,
366 pub input: Value,
367}
368
369impl ToolUseBlock {
370 pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
392 serde_json::from_value(self.input.clone()).ok()
393 }
394
395 pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
399 serde_json::from_value(self.input.clone())
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ToolResultBlock {
406 pub tool_use_id: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub content: Option<ToolResultContent>,
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub is_error: Option<bool>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(untagged)]
416pub enum ToolResultContent {
417 Text(String),
418 Structured(Vec<Value>),
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ResultMessage {
424 pub subtype: ResultSubtype,
425 pub is_error: bool,
426 pub duration_ms: u64,
427 pub duration_api_ms: u64,
428 pub num_turns: i32,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub result: Option<String>,
432
433 pub session_id: String,
434 pub total_cost_usd: f64,
435
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub usage: Option<UsageInfo>,
438
439 #[serde(default)]
441 pub permission_denials: Vec<PermissionDenial>,
442
443 #[serde(default)]
448 pub errors: Vec<String>,
449
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub uuid: Option<String>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
459pub struct PermissionDenial {
460 pub tool_name: String,
462
463 pub tool_input: Value,
465
466 pub tool_use_id: String,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum ResultSubtype {
474 Success,
475 ErrorMaxTurns,
476 ErrorDuringExecution,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum McpServerConfig {
483 Stdio(McpStdioServerConfig),
484 Sse(McpSseServerConfig),
485 Http(McpHttpServerConfig),
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct McpStdioServerConfig {
491 pub command: String,
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub args: Option<Vec<String>>,
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub env: Option<std::collections::HashMap<String, String>>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct McpSseServerConfig {
501 pub url: String,
502 #[serde(skip_serializing_if = "Option::is_none")]
503 pub headers: Option<std::collections::HashMap<String, String>>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct McpHttpServerConfig {
509 pub url: String,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub headers: Option<std::collections::HashMap<String, String>>,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub enum PermissionMode {
518 Default,
519 AcceptEdits,
520 BypassPermissions,
521 Plan,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct ControlRequest {
535 pub request_id: String,
537 pub request: ControlRequestPayload,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(tag = "subtype", rename_all = "snake_case")]
544pub enum ControlRequestPayload {
545 CanUseTool(ToolPermissionRequest),
547 HookCallback(HookCallbackRequest),
549 McpMessage(McpMessageRequest),
551 Initialize(InitializeRequest),
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
566pub struct PermissionSuggestion {
567 #[serde(rename = "type")]
569 pub suggestion_type: String,
570 pub destination: String,
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub mode: Option<String>,
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub behavior: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub rules: Option<Vec<Value>>,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct ToolPermissionRequest {
610 pub tool_name: String,
612 pub input: Value,
614 #[serde(default)]
616 pub permission_suggestions: Vec<PermissionSuggestion>,
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub blocked_path: Option<String>,
620}
621
622impl ToolPermissionRequest {
623 pub fn allow(&self, request_id: &str) -> ControlResponse {
638 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
639 }
640
641 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
661 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
662 }
663
664 pub fn allow_with_permissions(
666 &self,
667 modified_input: Value,
668 permissions: Vec<Value>,
669 request_id: &str,
670 ) -> ControlResponse {
671 ControlResponse::from_result(
672 request_id,
673 PermissionResult::allow_with_permissions(modified_input, permissions),
674 )
675 }
676
677 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
694 ControlResponse::from_result(request_id, PermissionResult::deny(message))
695 }
696
697 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
701 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
702 }
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
710#[serde(tag = "behavior", rename_all = "snake_case")]
711pub enum PermissionResult {
712 Allow {
714 #[serde(rename = "updatedInput")]
716 updated_input: Value,
717 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
719 updated_permissions: Option<Vec<Value>>,
720 },
721 Deny {
723 message: String,
725 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
727 interrupt: bool,
728 },
729}
730
731impl PermissionResult {
732 pub fn allow(input: Value) -> Self {
734 PermissionResult::Allow {
735 updated_input: input,
736 updated_permissions: None,
737 }
738 }
739
740 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
742 PermissionResult::Allow {
743 updated_input: input,
744 updated_permissions: Some(permissions),
745 }
746 }
747
748 pub fn deny(message: impl Into<String>) -> Self {
750 PermissionResult::Deny {
751 message: message.into(),
752 interrupt: false,
753 }
754 }
755
756 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
758 PermissionResult::Deny {
759 message: message.into(),
760 interrupt: true,
761 }
762 }
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct HookCallbackRequest {
768 pub callback_id: String,
769 pub input: Value,
770 #[serde(skip_serializing_if = "Option::is_none")]
771 pub tool_use_id: Option<String>,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct McpMessageRequest {
777 pub server_name: String,
778 pub message: Value,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct InitializeRequest {
784 #[serde(skip_serializing_if = "Option::is_none")]
785 pub hooks: Option<Value>,
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct ControlResponse {
794 pub response: ControlResponsePayload,
796}
797
798impl ControlResponse {
799 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
803 let response_value = serde_json::to_value(&result)
805 .expect("PermissionResult serialization should never fail");
806 ControlResponse {
807 response: ControlResponsePayload::Success {
808 request_id: request_id.to_string(),
809 response: Some(response_value),
810 },
811 }
812 }
813
814 pub fn success(request_id: &str, response_data: Value) -> Self {
816 ControlResponse {
817 response: ControlResponsePayload::Success {
818 request_id: request_id.to_string(),
819 response: Some(response_data),
820 },
821 }
822 }
823
824 pub fn success_empty(request_id: &str) -> Self {
826 ControlResponse {
827 response: ControlResponsePayload::Success {
828 request_id: request_id.to_string(),
829 response: None,
830 },
831 }
832 }
833
834 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
836 ControlResponse {
837 response: ControlResponsePayload::Error {
838 request_id: request_id.to_string(),
839 error: error_message.into(),
840 },
841 }
842 }
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
847#[serde(tag = "subtype", rename_all = "snake_case")]
848pub enum ControlResponsePayload {
849 Success {
850 request_id: String,
851 #[serde(skip_serializing_if = "Option::is_none")]
852 response: Option<Value>,
853 },
854 Error {
855 request_id: String,
856 error: String,
857 },
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct ControlResponseMessage {
863 #[serde(rename = "type")]
864 pub message_type: String,
865 pub response: ControlResponsePayload,
866}
867
868impl From<ControlResponse> for ControlResponseMessage {
869 fn from(resp: ControlResponse) -> Self {
870 ControlResponseMessage {
871 message_type: "control_response".to_string(),
872 response: resp.response,
873 }
874 }
875}
876
877#[derive(Debug, Clone, Serialize, Deserialize)]
879pub struct ControlRequestMessage {
880 #[serde(rename = "type")]
881 pub message_type: String,
882 pub request_id: String,
883 pub request: ControlRequestPayload,
884}
885
886impl ControlRequestMessage {
887 pub fn initialize(request_id: impl Into<String>) -> Self {
889 ControlRequestMessage {
890 message_type: "control_request".to_string(),
891 request_id: request_id.into(),
892 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
893 }
894 }
895
896 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
898 ControlRequestMessage {
899 message_type: "control_request".to_string(),
900 request_id: request_id.into(),
901 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
902 }
903 }
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct UsageInfo {
909 pub input_tokens: u32,
910 pub cache_creation_input_tokens: u32,
911 pub cache_read_input_tokens: u32,
912 pub output_tokens: u32,
913 pub server_tool_use: ServerToolUse,
914 pub service_tier: String,
915}
916
917#[derive(Debug, Clone, Serialize, Deserialize)]
919pub struct ServerToolUse {
920 pub web_search_requests: u32,
921}
922
923impl ClaudeInput {
924 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
926 ClaudeInput::User(UserMessage {
927 message: MessageContent {
928 role: "user".to_string(),
929 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
930 },
931 session_id: Some(session_id),
932 })
933 }
934
935 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
937 ClaudeInput::User(UserMessage {
938 message: MessageContent {
939 role: "user".to_string(),
940 content: blocks,
941 },
942 session_id: Some(session_id),
943 })
944 }
945
946 pub fn user_message_with_image(
949 image_data: String,
950 media_type: String,
951 text: Option<String>,
952 session_id: Uuid,
953 ) -> Result<Self, String> {
954 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
956
957 if !valid_types.contains(&media_type.as_str()) {
958 return Err(format!(
959 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
960 media_type
961 ));
962 }
963
964 let mut blocks = vec![ContentBlock::Image(ImageBlock {
965 source: ImageSource {
966 source_type: "base64".to_string(),
967 media_type,
968 data: image_data,
969 },
970 })];
971
972 if let Some(text_content) = text {
973 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
974 }
975
976 Ok(Self::user_message_blocks(blocks, session_id))
977 }
978}
979
980impl ClaudeOutput {
981 pub fn message_type(&self) -> String {
983 match self {
984 ClaudeOutput::System(_) => "system".to_string(),
985 ClaudeOutput::User(_) => "user".to_string(),
986 ClaudeOutput::Assistant(_) => "assistant".to_string(),
987 ClaudeOutput::Result(_) => "result".to_string(),
988 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
989 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
990 }
991 }
992
993 pub fn is_control_request(&self) -> bool {
995 matches!(self, ClaudeOutput::ControlRequest(_))
996 }
997
998 pub fn is_control_response(&self) -> bool {
1000 matches!(self, ClaudeOutput::ControlResponse(_))
1001 }
1002
1003 pub fn as_control_request(&self) -> Option<&ControlRequest> {
1005 match self {
1006 ClaudeOutput::ControlRequest(req) => Some(req),
1007 _ => None,
1008 }
1009 }
1010
1011 pub fn is_error(&self) -> bool {
1013 matches!(self, ClaudeOutput::Result(r) if r.is_error)
1014 }
1015
1016 pub fn is_assistant_message(&self) -> bool {
1018 matches!(self, ClaudeOutput::Assistant(_))
1019 }
1020
1021 pub fn is_system_message(&self) -> bool {
1023 matches!(self, ClaudeOutput::System(_))
1024 }
1025
1026 pub fn is_system_init(&self) -> bool {
1037 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1038 }
1039
1040 pub fn session_id(&self) -> Option<&str> {
1056 match self {
1057 ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1058 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1059 ClaudeOutput::Result(res) => Some(&res.session_id),
1060 ClaudeOutput::User(_) => None,
1061 ClaudeOutput::ControlRequest(_) => None,
1062 ClaudeOutput::ControlResponse(_) => None,
1063 }
1064 }
1065
1066 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1085 match self {
1086 ClaudeOutput::Assistant(ass) => {
1087 ass.message.content.iter().find_map(|block| match block {
1088 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1089 _ => None,
1090 })
1091 }
1092 _ => None,
1093 }
1094 }
1095
1096 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1116 let content = match self {
1117 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1118 _ => None,
1119 };
1120
1121 content
1122 .into_iter()
1123 .flat_map(|c| c.iter())
1124 .filter_map(|block| match block {
1125 ContentBlock::ToolUse(tu) => Some(tu),
1126 _ => None,
1127 })
1128 }
1129
1130 pub fn text_content(&self) -> Option<String> {
1146 match self {
1147 ClaudeOutput::Assistant(ass) => {
1148 let texts: Vec<&str> = ass
1149 .message
1150 .content
1151 .iter()
1152 .filter_map(|block| match block {
1153 ContentBlock::Text(t) => Some(t.text.as_str()),
1154 _ => None,
1155 })
1156 .collect();
1157
1158 if texts.is_empty() {
1159 None
1160 } else {
1161 Some(texts.join(""))
1162 }
1163 }
1164 _ => None,
1165 }
1166 }
1167
1168 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1183 match self {
1184 ClaudeOutput::Assistant(ass) => Some(ass),
1185 _ => None,
1186 }
1187 }
1188
1189 pub fn as_result(&self) -> Option<&ResultMessage> {
1205 match self {
1206 ClaudeOutput::Result(res) => Some(res),
1207 _ => None,
1208 }
1209 }
1210
1211 pub fn as_system(&self) -> Option<&SystemMessage> {
1213 match self {
1214 ClaudeOutput::System(sys) => Some(sys),
1215 _ => None,
1216 }
1217 }
1218
1219 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1224 match Self::parse_json(s) {
1226 Ok(output) => Ok(output),
1227 Err(first_error) => {
1228 if let Some(json_start) = s.find('{') {
1230 let trimmed = &s[json_start..];
1231 match Self::parse_json(trimmed) {
1232 Ok(output) => Ok(output),
1233 Err(_) => {
1234 Err(first_error)
1236 }
1237 }
1238 } else {
1239 Err(first_error)
1240 }
1241 }
1242 }
1243 }
1244
1245 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1247 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1249 raw_json: Value::String(s.to_string()),
1250 error_message: format!("Invalid JSON: {}", e),
1251 })?;
1252
1253 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1255 raw_json: value,
1256 error_message: e.to_string(),
1257 })
1258 }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263 use super::*;
1264
1265 #[test]
1266 fn test_serialize_user_message() {
1267 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1268 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1269 let json = serde_json::to_string(&input).unwrap();
1270 assert!(json.contains("\"type\":\"user\""));
1271 assert!(json.contains("\"role\":\"user\""));
1272 assert!(json.contains("\"text\":\"Hello, Claude!\""));
1273 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1274 }
1275
1276 #[test]
1277 fn test_deserialize_assistant_message() {
1278 let json = r#"{
1279 "type": "assistant",
1280 "message": {
1281 "id": "msg_123",
1282 "role": "assistant",
1283 "model": "claude-3-sonnet",
1284 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1285 },
1286 "session_id": "123"
1287 }"#;
1288
1289 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1290 assert!(output.is_assistant_message());
1291 }
1292
1293 #[test]
1294 fn test_deserialize_result_message() {
1295 let json = r#"{
1296 "type": "result",
1297 "subtype": "success",
1298 "is_error": false,
1299 "duration_ms": 100,
1300 "duration_api_ms": 200,
1301 "num_turns": 1,
1302 "result": "Done",
1303 "session_id": "123",
1304 "total_cost_usd": 0.01,
1305 "permission_denials": []
1306 }"#;
1307
1308 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1309 assert!(!output.is_error());
1310 }
1311
1312 #[test]
1313 fn test_deserialize_result_with_permission_denials() {
1314 let json = r#"{
1315 "type": "result",
1316 "subtype": "success",
1317 "is_error": false,
1318 "duration_ms": 100,
1319 "duration_api_ms": 200,
1320 "num_turns": 2,
1321 "result": "Done",
1322 "session_id": "123",
1323 "total_cost_usd": 0.01,
1324 "permission_denials": [
1325 {
1326 "tool_name": "Bash",
1327 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1328 "tool_use_id": "toolu_123"
1329 }
1330 ]
1331 }"#;
1332
1333 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1334 if let ClaudeOutput::Result(result) = output {
1335 assert_eq!(result.permission_denials.len(), 1);
1336 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1337 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1338 assert_eq!(
1339 result.permission_denials[0]
1340 .tool_input
1341 .get("command")
1342 .unwrap(),
1343 "rm -rf /"
1344 );
1345 } else {
1346 panic!("Expected Result");
1347 }
1348 }
1349
1350 #[test]
1351 fn test_permission_denial_roundtrip() {
1352 let denial = PermissionDenial {
1353 tool_name: "Write".to_string(),
1354 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1355 tool_use_id: "toolu_456".to_string(),
1356 };
1357
1358 let json = serde_json::to_string(&denial).unwrap();
1359 assert!(json.contains("\"tool_name\":\"Write\""));
1360 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1361 assert!(json.contains("/etc/passwd"));
1362
1363 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1364 assert_eq!(parsed, denial);
1365 }
1366
1367 #[test]
1372 fn test_deserialize_control_request_can_use_tool() {
1373 let json = r#"{
1374 "type": "control_request",
1375 "request_id": "perm-abc123",
1376 "request": {
1377 "subtype": "can_use_tool",
1378 "tool_name": "Write",
1379 "input": {
1380 "file_path": "/home/user/hello.py",
1381 "content": "print('hello')"
1382 },
1383 "permission_suggestions": [],
1384 "blocked_path": null
1385 }
1386 }"#;
1387
1388 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1389 assert!(output.is_control_request());
1390
1391 if let ClaudeOutput::ControlRequest(req) = output {
1392 assert_eq!(req.request_id, "perm-abc123");
1393 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1394 assert_eq!(perm_req.tool_name, "Write");
1395 assert_eq!(
1396 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1397 "/home/user/hello.py"
1398 );
1399 } else {
1400 panic!("Expected CanUseTool payload");
1401 }
1402 } else {
1403 panic!("Expected ControlRequest");
1404 }
1405 }
1406
1407 #[test]
1408 fn test_deserialize_control_request_edit_tool_real() {
1409 let json = r#"{"type":"control_request","request_id":"f3cf357c-17d6-4eca-b498-dd17c7ac43dd","request":{"subtype":"can_use_tool","tool_name":"Edit","input":{"file_path":"/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs","old_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal logout && claude-portal login\".bright_cyan()\n );\n}","new_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal --reauth\".bright_cyan()\n );\n}","replace_all":false},"permission_suggestions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}],"tool_use_id":"toolu_015BDGtNiqNrRSJSDrWXNckW"}}"#;
1411
1412 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1413 assert!(output.is_control_request());
1414 assert_eq!(output.message_type(), "control_request");
1415
1416 if let ClaudeOutput::ControlRequest(req) = output {
1417 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1418 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1419 assert_eq!(perm_req.tool_name, "Edit");
1420 assert_eq!(
1422 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1423 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1424 );
1425 assert!(perm_req.input.get("old_string").is_some());
1426 assert!(perm_req.input.get("new_string").is_some());
1427 assert!(!perm_req
1428 .input
1429 .get("replace_all")
1430 .unwrap()
1431 .as_bool()
1432 .unwrap());
1433 } else {
1434 panic!("Expected CanUseTool payload");
1435 }
1436 } else {
1437 panic!("Expected ControlRequest");
1438 }
1439 }
1440
1441 #[test]
1442 fn test_tool_permission_request_allow() {
1443 let req = ToolPermissionRequest {
1444 tool_name: "Read".to_string(),
1445 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1446 permission_suggestions: vec![],
1447 blocked_path: None,
1448 };
1449
1450 let response = req.allow("req-123");
1451 let message: ControlResponseMessage = response.into();
1452
1453 let json = serde_json::to_string(&message).unwrap();
1454 assert!(json.contains("\"type\":\"control_response\""));
1455 assert!(json.contains("\"subtype\":\"success\""));
1456 assert!(json.contains("\"request_id\":\"req-123\""));
1457 assert!(json.contains("\"behavior\":\"allow\""));
1458 assert!(json.contains("\"updatedInput\""));
1459 }
1460
1461 #[test]
1462 fn test_tool_permission_request_allow_with_modified_input() {
1463 let req = ToolPermissionRequest {
1464 tool_name: "Write".to_string(),
1465 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1466 permission_suggestions: vec![],
1467 blocked_path: None,
1468 };
1469
1470 let modified_input = serde_json::json!({
1471 "file_path": "/tmp/safe/passwd",
1472 "content": "test"
1473 });
1474 let response = req.allow_with(modified_input, "req-456");
1475 let message: ControlResponseMessage = response.into();
1476
1477 let json = serde_json::to_string(&message).unwrap();
1478 assert!(json.contains("/tmp/safe/passwd"));
1479 assert!(!json.contains("/etc/passwd"));
1480 }
1481
1482 #[test]
1483 fn test_tool_permission_request_deny() {
1484 let req = ToolPermissionRequest {
1485 tool_name: "Bash".to_string(),
1486 input: serde_json::json!({"command": "sudo rm -rf /"}),
1487 permission_suggestions: vec![],
1488 blocked_path: None,
1489 };
1490
1491 let response = req.deny("Dangerous command blocked", "req-789");
1492 let message: ControlResponseMessage = response.into();
1493
1494 let json = serde_json::to_string(&message).unwrap();
1495 assert!(json.contains("\"behavior\":\"deny\""));
1496 assert!(json.contains("Dangerous command blocked"));
1497 assert!(!json.contains("\"interrupt\":true"));
1498 }
1499
1500 #[test]
1501 fn test_tool_permission_request_deny_and_stop() {
1502 let req = ToolPermissionRequest {
1503 tool_name: "Bash".to_string(),
1504 input: serde_json::json!({"command": "rm -rf /"}),
1505 permission_suggestions: vec![],
1506 blocked_path: None,
1507 };
1508
1509 let response = req.deny_and_stop("Security violation", "req-000");
1510 let message: ControlResponseMessage = response.into();
1511
1512 let json = serde_json::to_string(&message).unwrap();
1513 assert!(json.contains("\"behavior\":\"deny\""));
1514 assert!(json.contains("\"interrupt\":true"));
1515 }
1516
1517 #[test]
1518 fn test_permission_result_serialization() {
1519 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1521 let json = serde_json::to_string(&allow).unwrap();
1522 assert!(json.contains("\"behavior\":\"allow\""));
1523 assert!(json.contains("\"updatedInput\""));
1524
1525 let deny = PermissionResult::deny("Not allowed");
1527 let json = serde_json::to_string(&deny).unwrap();
1528 assert!(json.contains("\"behavior\":\"deny\""));
1529 assert!(json.contains("\"message\":\"Not allowed\""));
1530 assert!(!json.contains("\"interrupt\""));
1531
1532 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1534 let json = serde_json::to_string(&deny_stop).unwrap();
1535 assert!(json.contains("\"interrupt\":true"));
1536 }
1537
1538 #[test]
1539 fn test_control_request_message_initialize() {
1540 let init = ControlRequestMessage::initialize("init-1");
1541
1542 let json = serde_json::to_string(&init).unwrap();
1543 assert!(json.contains("\"type\":\"control_request\""));
1544 assert!(json.contains("\"request_id\":\"init-1\""));
1545 assert!(json.contains("\"subtype\":\"initialize\""));
1546 }
1547
1548 #[test]
1549 fn test_control_response_error() {
1550 let response = ControlResponse::error("req-err", "Something went wrong");
1551 let message: ControlResponseMessage = response.into();
1552
1553 let json = serde_json::to_string(&message).unwrap();
1554 assert!(json.contains("\"subtype\":\"error\""));
1555 assert!(json.contains("\"error\":\"Something went wrong\""));
1556 }
1557
1558 #[test]
1559 fn test_roundtrip_control_request() {
1560 let original_json = r#"{
1562 "type": "control_request",
1563 "request_id": "test-123",
1564 "request": {
1565 "subtype": "can_use_tool",
1566 "tool_name": "Bash",
1567 "input": {"command": "ls -la"},
1568 "permission_suggestions": []
1569 }
1570 }"#;
1571
1572 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1574
1575 let reserialized = serde_json::to_string(&output).unwrap();
1577 assert!(reserialized.contains("control_request"));
1578 assert!(reserialized.contains("test-123"));
1579 assert!(reserialized.contains("Bash"));
1580 }
1581
1582 #[test]
1583 fn test_permission_suggestions_parsing() {
1584 let json = r#"{
1586 "type": "control_request",
1587 "request_id": "perm-456",
1588 "request": {
1589 "subtype": "can_use_tool",
1590 "tool_name": "Bash",
1591 "input": {"command": "npm test"},
1592 "permission_suggestions": [
1593 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1594 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1595 ]
1596 }
1597 }"#;
1598
1599 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1600 if let ClaudeOutput::ControlRequest(req) = output {
1601 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1602 assert_eq!(perm_req.permission_suggestions.len(), 2);
1603 assert_eq!(
1604 perm_req.permission_suggestions[0].suggestion_type,
1605 "setMode"
1606 );
1607 assert_eq!(
1608 perm_req.permission_suggestions[0].mode,
1609 Some("acceptEdits".to_string())
1610 );
1611 assert_eq!(perm_req.permission_suggestions[0].destination, "session");
1612 assert_eq!(
1613 perm_req.permission_suggestions[1].suggestion_type,
1614 "setMode"
1615 );
1616 assert_eq!(
1617 perm_req.permission_suggestions[1].mode,
1618 Some("bypassPermissions".to_string())
1619 );
1620 assert_eq!(perm_req.permission_suggestions[1].destination, "project");
1621 } else {
1622 panic!("Expected CanUseTool payload");
1623 }
1624 } else {
1625 panic!("Expected ControlRequest");
1626 }
1627 }
1628
1629 #[test]
1630 fn test_permission_suggestion_set_mode_roundtrip() {
1631 let suggestion = PermissionSuggestion {
1632 suggestion_type: "setMode".to_string(),
1633 destination: "session".to_string(),
1634 mode: Some("acceptEdits".to_string()),
1635 behavior: None,
1636 rules: None,
1637 };
1638
1639 let json = serde_json::to_string(&suggestion).unwrap();
1640 assert!(json.contains("\"type\":\"setMode\""));
1641 assert!(json.contains("\"mode\":\"acceptEdits\""));
1642 assert!(json.contains("\"destination\":\"session\""));
1643 assert!(!json.contains("\"behavior\""));
1644 assert!(!json.contains("\"rules\""));
1645
1646 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1647 assert_eq!(parsed, suggestion);
1648 }
1649
1650 #[test]
1651 fn test_permission_suggestion_add_rules_roundtrip() {
1652 let suggestion = PermissionSuggestion {
1653 suggestion_type: "addRules".to_string(),
1654 destination: "session".to_string(),
1655 mode: None,
1656 behavior: Some("allow".to_string()),
1657 rules: Some(vec![serde_json::json!({
1658 "toolName": "Read",
1659 "ruleContent": "//tmp/**"
1660 })]),
1661 };
1662
1663 let json = serde_json::to_string(&suggestion).unwrap();
1664 assert!(json.contains("\"type\":\"addRules\""));
1665 assert!(json.contains("\"behavior\":\"allow\""));
1666 assert!(json.contains("\"destination\":\"session\""));
1667 assert!(json.contains("\"rules\""));
1668 assert!(json.contains("\"toolName\":\"Read\""));
1669 assert!(!json.contains("\"mode\""));
1670
1671 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1672 assert_eq!(parsed, suggestion);
1673 }
1674
1675 #[test]
1676 fn test_permission_suggestion_add_rules_from_real_json() {
1677 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1679
1680 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1681 assert_eq!(parsed.suggestion_type, "addRules");
1682 assert_eq!(parsed.destination, "session");
1683 assert_eq!(parsed.behavior, Some("allow".to_string()));
1684 assert!(parsed.rules.is_some());
1685 assert!(parsed.mode.is_none());
1686 }
1687
1688 #[test]
1693 fn test_system_message_init() {
1694 let json = r#"{
1695 "type": "system",
1696 "subtype": "init",
1697 "session_id": "test-session-123",
1698 "cwd": "/home/user/project",
1699 "model": "claude-sonnet-4",
1700 "tools": ["Bash", "Read", "Write"],
1701 "mcp_servers": []
1702 }"#;
1703
1704 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1705 if let ClaudeOutput::System(sys) = output {
1706 assert!(sys.is_init());
1707 assert!(!sys.is_status());
1708 assert!(!sys.is_compact_boundary());
1709
1710 let init = sys.as_init().expect("Should parse as init");
1711 assert_eq!(init.session_id, "test-session-123");
1712 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1713 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1714 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1715 } else {
1716 panic!("Expected System message");
1717 }
1718 }
1719
1720 #[test]
1721 fn test_system_message_status() {
1722 let json = r#"{
1723 "type": "system",
1724 "subtype": "status",
1725 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1726 "status": "compacting",
1727 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1728 }"#;
1729
1730 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1731 if let ClaudeOutput::System(sys) = output {
1732 assert!(sys.is_status());
1733 assert!(!sys.is_init());
1734
1735 let status = sys.as_status().expect("Should parse as status");
1736 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1737 assert_eq!(status.status, Some("compacting".to_string()));
1738 assert_eq!(
1739 status.uuid,
1740 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1741 );
1742 } else {
1743 panic!("Expected System message");
1744 }
1745 }
1746
1747 #[test]
1748 fn test_system_message_status_null() {
1749 let json = r#"{
1750 "type": "system",
1751 "subtype": "status",
1752 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1753 "status": null,
1754 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1755 }"#;
1756
1757 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1758 if let ClaudeOutput::System(sys) = output {
1759 let status = sys.as_status().expect("Should parse as status");
1760 assert_eq!(status.status, None);
1761 } else {
1762 panic!("Expected System message");
1763 }
1764 }
1765
1766 #[test]
1767 fn test_system_message_compact_boundary() {
1768 let json = r#"{
1769 "type": "system",
1770 "subtype": "compact_boundary",
1771 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1772 "compact_metadata": {
1773 "pre_tokens": 155285,
1774 "trigger": "auto"
1775 },
1776 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1777 }"#;
1778
1779 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1780 if let ClaudeOutput::System(sys) = output {
1781 assert!(sys.is_compact_boundary());
1782 assert!(!sys.is_init());
1783 assert!(!sys.is_status());
1784
1785 let compact = sys
1786 .as_compact_boundary()
1787 .expect("Should parse as compact_boundary");
1788 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1789 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1790 assert_eq!(compact.compact_metadata.trigger, "auto");
1791 } else {
1792 panic!("Expected System message");
1793 }
1794 }
1795
1796 #[test]
1801 fn test_is_system_init() {
1802 let init_json = r#"{
1803 "type": "system",
1804 "subtype": "init",
1805 "session_id": "test-session"
1806 }"#;
1807 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1808 assert!(output.is_system_init());
1809
1810 let status_json = r#"{
1811 "type": "system",
1812 "subtype": "status",
1813 "session_id": "test-session"
1814 }"#;
1815 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1816 assert!(!output.is_system_init());
1817 }
1818
1819 #[test]
1820 fn test_session_id() {
1821 let result_json = r#"{
1823 "type": "result",
1824 "subtype": "success",
1825 "is_error": false,
1826 "duration_ms": 100,
1827 "duration_api_ms": 200,
1828 "num_turns": 1,
1829 "session_id": "result-session",
1830 "total_cost_usd": 0.01
1831 }"#;
1832 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1833 assert_eq!(output.session_id(), Some("result-session"));
1834
1835 let assistant_json = r#"{
1837 "type": "assistant",
1838 "message": {
1839 "id": "msg_1",
1840 "role": "assistant",
1841 "model": "claude-3",
1842 "content": []
1843 },
1844 "session_id": "assistant-session"
1845 }"#;
1846 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1847 assert_eq!(output.session_id(), Some("assistant-session"));
1848
1849 let system_json = r#"{
1851 "type": "system",
1852 "subtype": "init",
1853 "session_id": "system-session"
1854 }"#;
1855 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1856 assert_eq!(output.session_id(), Some("system-session"));
1857 }
1858
1859 #[test]
1860 fn test_as_tool_use() {
1861 let json = r#"{
1862 "type": "assistant",
1863 "message": {
1864 "id": "msg_1",
1865 "role": "assistant",
1866 "model": "claude-3",
1867 "content": [
1868 {"type": "text", "text": "Let me run that command."},
1869 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1870 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1871 ]
1872 },
1873 "session_id": "abc"
1874 }"#;
1875 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1876
1877 let bash = output.as_tool_use("Bash");
1879 assert!(bash.is_some());
1880 assert_eq!(bash.unwrap().id, "tu_1");
1881
1882 let read = output.as_tool_use("Read");
1884 assert!(read.is_some());
1885 assert_eq!(read.unwrap().id, "tu_2");
1886
1887 assert!(output.as_tool_use("Write").is_none());
1889
1890 let result_json = r#"{
1892 "type": "result",
1893 "subtype": "success",
1894 "is_error": false,
1895 "duration_ms": 100,
1896 "duration_api_ms": 200,
1897 "num_turns": 1,
1898 "session_id": "abc",
1899 "total_cost_usd": 0.01
1900 }"#;
1901 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1902 assert!(result.as_tool_use("Bash").is_none());
1903 }
1904
1905 #[test]
1906 fn test_tool_uses() {
1907 let json = r#"{
1908 "type": "assistant",
1909 "message": {
1910 "id": "msg_1",
1911 "role": "assistant",
1912 "model": "claude-3",
1913 "content": [
1914 {"type": "text", "text": "Running commands..."},
1915 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1916 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1917 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1918 ]
1919 },
1920 "session_id": "abc"
1921 }"#;
1922 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1923
1924 let tools: Vec<_> = output.tool_uses().collect();
1925 assert_eq!(tools.len(), 3);
1926 assert_eq!(tools[0].name, "Bash");
1927 assert_eq!(tools[1].name, "Read");
1928 assert_eq!(tools[2].name, "Write");
1929 }
1930
1931 #[test]
1932 fn test_text_content() {
1933 let json = r#"{
1935 "type": "assistant",
1936 "message": {
1937 "id": "msg_1",
1938 "role": "assistant",
1939 "model": "claude-3",
1940 "content": [{"type": "text", "text": "Hello, world!"}]
1941 },
1942 "session_id": "abc"
1943 }"#;
1944 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1945 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1946
1947 let json = r#"{
1949 "type": "assistant",
1950 "message": {
1951 "id": "msg_1",
1952 "role": "assistant",
1953 "model": "claude-3",
1954 "content": [
1955 {"type": "text", "text": "Hello, "},
1956 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1957 {"type": "text", "text": "world!"}
1958 ]
1959 },
1960 "session_id": "abc"
1961 }"#;
1962 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1963 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1964
1965 let json = r#"{
1967 "type": "assistant",
1968 "message": {
1969 "id": "msg_1",
1970 "role": "assistant",
1971 "model": "claude-3",
1972 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1973 },
1974 "session_id": "abc"
1975 }"#;
1976 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1977 assert_eq!(output.text_content(), None);
1978
1979 let json = r#"{
1981 "type": "result",
1982 "subtype": "success",
1983 "is_error": false,
1984 "duration_ms": 100,
1985 "duration_api_ms": 200,
1986 "num_turns": 1,
1987 "session_id": "abc",
1988 "total_cost_usd": 0.01
1989 }"#;
1990 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1991 assert_eq!(output.text_content(), None);
1992 }
1993
1994 #[test]
1995 fn test_as_assistant() {
1996 let json = r#"{
1997 "type": "assistant",
1998 "message": {
1999 "id": "msg_1",
2000 "role": "assistant",
2001 "model": "claude-sonnet-4",
2002 "content": []
2003 },
2004 "session_id": "abc"
2005 }"#;
2006 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2007
2008 let assistant = output.as_assistant();
2009 assert!(assistant.is_some());
2010 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
2011
2012 let result_json = r#"{
2014 "type": "result",
2015 "subtype": "success",
2016 "is_error": false,
2017 "duration_ms": 100,
2018 "duration_api_ms": 200,
2019 "num_turns": 1,
2020 "session_id": "abc",
2021 "total_cost_usd": 0.01
2022 }"#;
2023 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2024 assert!(result.as_assistant().is_none());
2025 }
2026
2027 #[test]
2028 fn test_as_result() {
2029 let json = r#"{
2030 "type": "result",
2031 "subtype": "success",
2032 "is_error": false,
2033 "duration_ms": 100,
2034 "duration_api_ms": 200,
2035 "num_turns": 5,
2036 "session_id": "abc",
2037 "total_cost_usd": 0.05
2038 }"#;
2039 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2040
2041 let result = output.as_result();
2042 assert!(result.is_some());
2043 assert_eq!(result.unwrap().num_turns, 5);
2044 assert_eq!(result.unwrap().total_cost_usd, 0.05);
2045
2046 let assistant_json = r#"{
2048 "type": "assistant",
2049 "message": {
2050 "id": "msg_1",
2051 "role": "assistant",
2052 "model": "claude-3",
2053 "content": []
2054 },
2055 "session_id": "abc"
2056 }"#;
2057 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2058 assert!(assistant.as_result().is_none());
2059 }
2060
2061 #[test]
2062 fn test_as_system() {
2063 let json = r#"{
2064 "type": "system",
2065 "subtype": "init",
2066 "session_id": "abc",
2067 "model": "claude-3"
2068 }"#;
2069 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2070
2071 let system = output.as_system();
2072 assert!(system.is_some());
2073 assert!(system.unwrap().is_init());
2074
2075 let result_json = r#"{
2077 "type": "result",
2078 "subtype": "success",
2079 "is_error": false,
2080 "duration_ms": 100,
2081 "duration_api_ms": 200,
2082 "num_turns": 1,
2083 "session_id": "abc",
2084 "total_cost_usd": 0.01
2085 }"#;
2086 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2087 assert!(result.as_system().is_none());
2088 }
2089
2090 #[test]
2095 fn test_deserialize_result_message_with_errors() {
2096 let json = r#"{
2097 "type": "result",
2098 "subtype": "error_during_execution",
2099 "duration_ms": 0,
2100 "duration_api_ms": 0,
2101 "is_error": true,
2102 "num_turns": 0,
2103 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2104 "total_cost_usd": 0,
2105 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2106 "permission_denials": []
2107 }"#;
2108
2109 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2110 assert!(output.is_error());
2111
2112 if let ClaudeOutput::Result(res) = output {
2113 assert!(res.is_error);
2114 assert_eq!(res.errors.len(), 1);
2115 assert!(res.errors[0].contains("No conversation found"));
2116 } else {
2117 panic!("Expected Result message");
2118 }
2119 }
2120
2121 #[test]
2122 fn test_deserialize_result_message_errors_defaults_empty() {
2123 let json = r#"{
2125 "type": "result",
2126 "subtype": "success",
2127 "is_error": false,
2128 "duration_ms": 100,
2129 "duration_api_ms": 200,
2130 "num_turns": 1,
2131 "session_id": "123",
2132 "total_cost_usd": 0.01
2133 }"#;
2134
2135 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2136 if let ClaudeOutput::Result(res) = output {
2137 assert!(res.errors.is_empty());
2138 } else {
2139 panic!("Expected Result message");
2140 }
2141 }
2142
2143 #[test]
2144 fn test_result_message_errors_roundtrip() {
2145 let json = r#"{
2146 "type": "result",
2147 "subtype": "error_during_execution",
2148 "is_error": true,
2149 "duration_ms": 0,
2150 "duration_api_ms": 0,
2151 "num_turns": 0,
2152 "session_id": "test-session",
2153 "total_cost_usd": 0.0,
2154 "errors": ["Error 1", "Error 2"]
2155 }"#;
2156
2157 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2158 let reserialized = serde_json::to_string(&output).unwrap();
2159
2160 assert!(reserialized.contains("Error 1"));
2162 assert!(reserialized.contains("Error 2"));
2163 }
2164}