1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Transport {
31 Stdio,
33 Http,
35 Sse,
37}
38
39impl fmt::Display for Transport {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Stdio => write!(f, "stdio"),
43 Self::Http => write!(f, "http"),
44 Self::Sse => write!(f, "sse"),
45 }
46 }
47}
48
49#[derive(Debug, Clone, thiserror::Error)]
51#[error("unknown transport: {0}")]
52pub struct TransportParseError(pub String);
53
54impl FromStr for Transport {
55 type Err = TransportParseError;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 match s {
59 "stdio" => Ok(Self::Stdio),
60 "http" => Ok(Self::Http),
61 "sse" => Ok(Self::Sse),
62 other => Err(TransportParseError(other.to_string())),
63 }
64 }
65}
66
67impl TryFrom<&str> for Transport {
68 type Error = TransportParseError;
69
70 fn try_from(s: &str) -> Result<Self, Self::Error> {
71 s.parse()
72 }
73}
74
75#[cfg(test)]
76mod transport_tests {
77 use super::*;
78
79 #[test]
80 fn from_str_accepts_known_variants() {
81 assert_eq!("stdio".parse::<Transport>().unwrap(), Transport::Stdio);
82 assert_eq!("http".parse::<Transport>().unwrap(), Transport::Http);
83 assert_eq!("sse".parse::<Transport>().unwrap(), Transport::Sse);
84 }
85
86 #[test]
87 fn from_str_returns_error_for_unknown() {
88 let err = "websocket".parse::<Transport>().unwrap_err();
89 assert!(err.to_string().contains("websocket"));
90 }
91
92 #[test]
93 fn try_from_str_returns_error_for_unknown() {
94 assert!(Transport::try_from("bogus").is_err());
95 }
96}
97
98#[derive(Debug, Clone, Copy, Default)]
100pub enum OutputFormat {
101 #[default]
103 Text,
104 Json,
106 StreamJson,
108}
109
110impl OutputFormat {
111 pub(crate) fn as_arg(&self) -> &'static str {
112 match self {
113 Self::Text => "text",
114 Self::Json => "json",
115 Self::StreamJson => "stream-json",
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub enum PermissionMode {
124 #[default]
126 Default,
127 AcceptEdits,
129 #[deprecated(
141 since = "0.5.1",
142 note = "use claude_wrapper::dangerous::DangerousClient instead; \
143 direct BypassPermissions usage is a footgun and will go \
144 away in a future major release"
145 )]
146 BypassPermissions,
147 DontAsk,
149 Plan,
151 Auto,
153}
154
155impl PermissionMode {
156 pub(crate) fn as_arg(&self) -> &'static str {
157 match self {
158 Self::Default => "default",
159 Self::AcceptEdits => "acceptEdits",
160 #[allow(deprecated)]
161 Self::BypassPermissions => "bypassPermissions",
162 Self::DontAsk => "dontAsk",
163 Self::Plan => "plan",
164 Self::Auto => "auto",
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, Default)]
171pub enum InputFormat {
172 #[default]
174 Text,
175 StreamJson,
177}
178
179impl InputFormat {
180 pub(crate) fn as_arg(&self) -> &'static str {
181 match self {
182 Self::Text => "text",
183 Self::StreamJson => "stream-json",
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "lowercase")]
191pub enum Effort {
192 Low,
194 Medium,
196 High,
198 Xhigh,
200 Max,
202}
203
204impl Effort {
205 pub(crate) fn as_arg(&self) -> &'static str {
206 match self {
207 Self::Low => "low",
208 Self::Medium => "medium",
209 Self::High => "high",
210 Self::Xhigh => "xhigh",
211 Self::Max => "max",
212 }
213 }
214}
215
216#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
218pub enum Scope {
219 #[default]
221 Local,
222 User,
224 Project,
226 Managed,
230}
231
232impl Scope {
233 pub(crate) fn as_arg(&self) -> &'static str {
234 match self {
235 Self::Local => "local",
236 Self::User => "user",
237 Self::Project => "project",
238 Self::Managed => "managed",
239 }
240 }
241}
242
243#[cfg(feature = "json")]
260#[derive(Debug, Clone, Deserialize, Serialize)]
261#[serde(rename_all = "camelCase")]
262pub struct AuthStatus {
263 #[serde(default)]
265 pub logged_in: bool,
266 #[serde(default)]
268 pub auth_method: Option<String>,
269 #[serde(default)]
271 pub api_provider: Option<String>,
272 #[serde(default)]
274 pub email: Option<String>,
275 #[serde(default)]
277 pub org_id: Option<String>,
278 #[serde(default)]
280 pub org_name: Option<String>,
281 #[serde(default)]
283 pub subscription_type: Option<String>,
284 #[serde(flatten)]
286 pub extra: std::collections::HashMap<String, serde_json::Value>,
287}
288
289#[cfg(feature = "json")]
291#[derive(Debug, Clone, Deserialize, Serialize)]
292pub struct QueryMessage {
293 #[serde(default)]
295 pub role: String,
296 #[serde(default)]
298 pub content: serde_json::Value,
299 #[serde(flatten)]
301 pub extra: std::collections::HashMap<String, serde_json::Value>,
302}
303
304#[cfg(feature = "json")]
306#[derive(Debug, Clone, Deserialize, Serialize)]
307pub struct QueryResult {
308 #[serde(default)]
310 pub result: String,
311 #[serde(default)]
313 pub session_id: String,
314 #[serde(default, rename = "total_cost_usd", alias = "cost_usd")]
316 pub cost_usd: Option<f64>,
317 #[serde(default)]
319 pub duration_ms: Option<u64>,
320 #[serde(default)]
322 pub num_turns: Option<u32>,
323 #[serde(default)]
325 pub is_error: bool,
326 #[serde(flatten)]
328 pub extra: std::collections::HashMap<String, serde_json::Value>,
329}
330
331#[cfg(all(test, feature = "json"))]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn query_result_deserializes_total_cost_usd() {
337 let json =
338 r#"{"result":"hello","session_id":"s1","total_cost_usd":0.042,"is_error":false}"#;
339 let qr: QueryResult = serde_json::from_str(json).unwrap();
340 assert_eq!(qr.cost_usd, Some(0.042));
341 }
342
343 #[test]
344 fn query_result_deserializes_cost_usd_alias() {
345 let json = r#"{"result":"hello","session_id":"s1","cost_usd":0.01,"is_error":false}"#;
346 let qr: QueryResult = serde_json::from_str(json).unwrap();
347 assert_eq!(qr.cost_usd, Some(0.01));
348 }
349
350 #[test]
351 fn query_result_missing_cost_defaults_to_none() {
352 let json = r#"{"result":"hello","session_id":"s1","is_error":false}"#;
353 let qr: QueryResult = serde_json::from_str(json).unwrap();
354 assert_eq!(qr.cost_usd, None);
355 }
356
357 #[test]
358 fn query_result_from_stream_result_event() {
359 let json = r#"{"type":"result","subtype":"success","result":"streamed","session_id":"sess-1","total_cost_usd":0.03,"num_turns":1,"is_error":false}"#;
363 let qr: QueryResult = serde_json::from_str(json).unwrap();
364 assert_eq!(qr.cost_usd, Some(0.03));
365 assert_eq!(qr.num_turns, Some(1));
366 assert_eq!(qr.session_id, "sess-1");
367 assert_eq!(qr.result, "streamed");
368 assert_eq!(
369 qr.extra.get("type").and_then(|v| v.as_str()),
370 Some("result")
371 );
372 }
373
374 #[test]
375 fn query_result_deserializes_num_turns() {
376 let json = r#"{"result":"done","session_id":"s2","total_cost_usd":0.1,"num_turns":5,"is_error":false}"#;
377 let qr: QueryResult = serde_json::from_str(json).unwrap();
378 assert_eq!(qr.num_turns, Some(5));
379 assert_eq!(qr.cost_usd, Some(0.1));
380 }
381
382 #[test]
383 fn query_result_serializes_as_total_cost_usd() {
384 let qr = QueryResult {
385 result: "ok".into(),
386 session_id: "s1".into(),
387 cost_usd: Some(0.05),
388 duration_ms: None,
389 num_turns: Some(3),
390 is_error: false,
391 extra: Default::default(),
392 };
393 let json = serde_json::to_string(&qr).unwrap();
394 assert!(json.contains("\"total_cost_usd\""));
395 assert!(json.contains("\"num_turns\""));
396 }
397}