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")]
355#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
356pub struct TokenUsage {
357 #[serde(default)]
359 pub input_tokens: Option<u64>,
360 #[serde(default, alias = "cache_read_input_tokens")]
362 pub cached_input_tokens: Option<u64>,
363 #[serde(default, alias = "cache_creation_input_tokens")]
365 pub cache_write_input_tokens: Option<u64>,
366 #[serde(default)]
368 pub output_tokens: Option<u64>,
369 #[serde(default)]
372 pub reasoning_output_tokens: Option<u64>,
373 #[serde(default)]
375 pub total_tokens: Option<u64>,
376}
377
378#[cfg(feature = "json")]
379impl TokenUsage {
380 pub fn total(&self) -> u64 {
385 if let Some(t) = self.total_tokens {
386 return t;
387 }
388 [
389 self.input_tokens,
390 self.cached_input_tokens,
391 self.cache_write_input_tokens,
392 self.output_tokens,
393 self.reasoning_output_tokens,
394 ]
395 .iter()
396 .flatten()
397 .sum()
398 }
399
400 pub fn is_empty(&self) -> bool {
402 *self == Self::default()
403 }
404}
405
406#[cfg(feature = "json")]
408#[derive(Debug, Clone, Deserialize, Serialize)]
409pub struct QueryResult {
410 #[serde(default)]
412 pub result: String,
413 #[serde(default)]
415 pub session_id: String,
416 #[serde(default, rename = "total_cost_usd", alias = "cost_usd")]
418 pub cost_usd: Option<f64>,
419 #[serde(default)]
421 pub duration_ms: Option<u64>,
422 #[serde(default)]
424 pub num_turns: Option<u32>,
425 #[serde(default)]
427 pub is_error: bool,
428 #[serde(default)]
431 pub usage: Option<TokenUsage>,
432 #[serde(flatten)]
434 pub extra: std::collections::HashMap<String, serde_json::Value>,
435}
436
437#[cfg(all(test, feature = "json"))]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn query_result_deserializes_total_cost_usd() {
443 let json =
444 r#"{"result":"hello","session_id":"s1","total_cost_usd":0.042,"is_error":false}"#;
445 let qr: QueryResult = serde_json::from_str(json).unwrap();
446 assert_eq!(qr.cost_usd, Some(0.042));
447 }
448
449 #[test]
450 fn query_result_deserializes_cost_usd_alias() {
451 let json = r#"{"result":"hello","session_id":"s1","cost_usd":0.01,"is_error":false}"#;
452 let qr: QueryResult = serde_json::from_str(json).unwrap();
453 assert_eq!(qr.cost_usd, Some(0.01));
454 }
455
456 #[test]
457 fn query_result_missing_cost_defaults_to_none() {
458 let json = r#"{"result":"hello","session_id":"s1","is_error":false}"#;
459 let qr: QueryResult = serde_json::from_str(json).unwrap();
460 assert_eq!(qr.cost_usd, None);
461 }
462
463 #[test]
464 fn query_result_from_stream_result_event() {
465 let json = r#"{"type":"result","subtype":"success","result":"streamed","session_id":"sess-1","total_cost_usd":0.03,"num_turns":1,"is_error":false}"#;
469 let qr: QueryResult = serde_json::from_str(json).unwrap();
470 assert_eq!(qr.cost_usd, Some(0.03));
471 assert_eq!(qr.num_turns, Some(1));
472 assert_eq!(qr.session_id, "sess-1");
473 assert_eq!(qr.result, "streamed");
474 assert_eq!(
475 qr.extra.get("type").and_then(|v| v.as_str()),
476 Some("result")
477 );
478 }
479
480 #[test]
481 fn query_result_deserializes_num_turns() {
482 let json = r#"{"result":"done","session_id":"s2","total_cost_usd":0.1,"num_turns":5,"is_error":false}"#;
483 let qr: QueryResult = serde_json::from_str(json).unwrap();
484 assert_eq!(qr.num_turns, Some(5));
485 assert_eq!(qr.cost_usd, Some(0.1));
486 }
487
488 #[test]
489 fn query_result_serializes_as_total_cost_usd() {
490 let qr = QueryResult {
491 result: "ok".into(),
492 session_id: "s1".into(),
493 cost_usd: Some(0.05),
494 duration_ms: None,
495 num_turns: Some(3),
496 is_error: false,
497 usage: None,
498 extra: Default::default(),
499 };
500 let json = serde_json::to_string(&qr).unwrap();
501 assert!(json.contains("\"total_cost_usd\""));
502 assert!(json.contains("\"num_turns\""));
503 }
504
505 #[test]
506 fn token_usage_deserializes_anthropic_cache_key_names() {
507 let json = r#"{
508 "input_tokens": 100,
509 "cache_read_input_tokens": 400,
510 "cache_creation_input_tokens": 50,
511 "output_tokens": 25
512 }"#;
513 let usage: TokenUsage = serde_json::from_str(json).unwrap();
514 assert_eq!(usage.input_tokens, Some(100));
515 assert_eq!(usage.cached_input_tokens, Some(400));
516 assert_eq!(usage.cache_write_input_tokens, Some(50));
517 assert_eq!(usage.output_tokens, Some(25));
518 assert_eq!(usage.total(), 575);
519 assert!(!usage.is_empty());
520 }
521
522 #[test]
523 fn token_usage_prefers_explicit_total() {
524 let json = r#"{"input_tokens": 100, "output_tokens": 25, "total_tokens": 999}"#;
525 let usage: TokenUsage = serde_json::from_str(json).unwrap();
526 assert_eq!(usage.total(), 999);
527 }
528
529 #[test]
530 fn token_usage_empty_object_is_empty() {
531 let usage: TokenUsage = serde_json::from_str("{}").unwrap();
532 assert!(usage.is_empty());
533 assert_eq!(usage.total(), 0);
534 }
535
536 #[test]
537 fn query_result_parses_usage_off_result_event() {
538 let json = r#"{
539 "result": "hi",
540 "session_id": "s1",
541 "total_cost_usd": 0.01,
542 "is_error": false,
543 "usage": {"input_tokens": 7, "output_tokens": 3}
544 }"#;
545 let qr: QueryResult = serde_json::from_str(json).unwrap();
546 let usage = qr.usage.expect("usage parsed");
547 assert_eq!(usage.total(), 10);
548 assert!(!qr.extra.contains_key("usage"));
550 }
551}