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)]
231pub enum HermeticScope {
232 #[default]
236 Full,
237 Project,
241}
242
243impl HermeticScope {
244 pub(crate) fn setting_sources_value(self) -> &'static str {
246 match self {
247 Self::Full => "",
248 Self::Project => "user",
249 }
250 }
251}
252
253#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
255pub enum Scope {
256 #[default]
258 Local,
259 User,
261 Project,
263 Managed,
267}
268
269impl Scope {
270 pub(crate) fn as_arg(&self) -> &'static str {
271 match self {
272 Self::Local => "local",
273 Self::User => "user",
274 Self::Project => "project",
275 Self::Managed => "managed",
276 }
277 }
278}
279
280#[cfg(feature = "json")]
297#[derive(Debug, Clone, Deserialize, Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct AuthStatus {
300 #[serde(default)]
302 pub logged_in: bool,
303 #[serde(default)]
305 pub auth_method: Option<String>,
306 #[serde(default)]
308 pub api_provider: Option<String>,
309 #[serde(default)]
311 pub email: Option<String>,
312 #[serde(default)]
314 pub org_id: Option<String>,
315 #[serde(default)]
317 pub org_name: Option<String>,
318 #[serde(default)]
320 pub subscription_type: Option<String>,
321 #[serde(flatten)]
323 pub extra: std::collections::HashMap<String, serde_json::Value>,
324}
325
326#[cfg(feature = "json")]
328#[derive(Debug, Clone, Deserialize, Serialize)]
329pub struct QueryMessage {
330 #[serde(default)]
332 pub role: String,
333 #[serde(default)]
335 pub content: serde_json::Value,
336 #[serde(flatten)]
338 pub extra: std::collections::HashMap<String, serde_json::Value>,
339}
340
341#[cfg(feature = "json")]
343#[derive(Debug, Clone, Deserialize, Serialize)]
344pub struct QueryResult {
345 #[serde(default)]
347 pub result: String,
348 #[serde(default)]
350 pub session_id: String,
351 #[serde(default, rename = "total_cost_usd", alias = "cost_usd")]
353 pub cost_usd: Option<f64>,
354 #[serde(default)]
356 pub duration_ms: Option<u64>,
357 #[serde(default)]
359 pub num_turns: Option<u32>,
360 #[serde(default)]
362 pub is_error: bool,
363 #[serde(flatten)]
365 pub extra: std::collections::HashMap<String, serde_json::Value>,
366}
367
368#[cfg(all(test, feature = "json"))]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn query_result_deserializes_total_cost_usd() {
374 let json =
375 r#"{"result":"hello","session_id":"s1","total_cost_usd":0.042,"is_error":false}"#;
376 let qr: QueryResult = serde_json::from_str(json).unwrap();
377 assert_eq!(qr.cost_usd, Some(0.042));
378 }
379
380 #[test]
381 fn query_result_deserializes_cost_usd_alias() {
382 let json = r#"{"result":"hello","session_id":"s1","cost_usd":0.01,"is_error":false}"#;
383 let qr: QueryResult = serde_json::from_str(json).unwrap();
384 assert_eq!(qr.cost_usd, Some(0.01));
385 }
386
387 #[test]
388 fn query_result_missing_cost_defaults_to_none() {
389 let json = r#"{"result":"hello","session_id":"s1","is_error":false}"#;
390 let qr: QueryResult = serde_json::from_str(json).unwrap();
391 assert_eq!(qr.cost_usd, None);
392 }
393
394 #[test]
395 fn query_result_from_stream_result_event() {
396 let json = r#"{"type":"result","subtype":"success","result":"streamed","session_id":"sess-1","total_cost_usd":0.03,"num_turns":1,"is_error":false}"#;
400 let qr: QueryResult = serde_json::from_str(json).unwrap();
401 assert_eq!(qr.cost_usd, Some(0.03));
402 assert_eq!(qr.num_turns, Some(1));
403 assert_eq!(qr.session_id, "sess-1");
404 assert_eq!(qr.result, "streamed");
405 assert_eq!(
406 qr.extra.get("type").and_then(|v| v.as_str()),
407 Some("result")
408 );
409 }
410
411 #[test]
412 fn query_result_deserializes_num_turns() {
413 let json = r#"{"result":"done","session_id":"s2","total_cost_usd":0.1,"num_turns":5,"is_error":false}"#;
414 let qr: QueryResult = serde_json::from_str(json).unwrap();
415 assert_eq!(qr.num_turns, Some(5));
416 assert_eq!(qr.cost_usd, Some(0.1));
417 }
418
419 #[test]
420 fn query_result_serializes_as_total_cost_usd() {
421 let qr = QueryResult {
422 result: "ok".into(),
423 session_id: "s1".into(),
424 cost_usd: Some(0.05),
425 duration_ms: None,
426 num_turns: Some(3),
427 is_error: false,
428 extra: Default::default(),
429 };
430 let json = serde_json::to_string(&qr).unwrap();
431 assert!(json.contains("\"total_cost_usd\""));
432 assert!(json.contains("\"num_turns\""));
433 }
434}