Skip to main content

claude_wrapper/
types.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6/// Transport type for MCP server connections.
7///
8/// # Example
9///
10/// ```
11/// use claude_wrapper::Transport;
12/// use std::str::FromStr;
13///
14/// let t = Transport::from_str("stdio").unwrap();
15/// assert_eq!(t, Transport::Stdio);
16/// assert_eq!(t.to_string(), "stdio");
17///
18/// let t: Transport = "http".parse().unwrap();
19/// assert_eq!(t, Transport::Http);
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum Transport {
24    /// Standard I/O transport — server runs as a subprocess.
25    Stdio,
26    /// HTTP transport — server accessible via URL.
27    Http,
28    /// Server-Sent Events transport — server accessible via URL with SSE.
29    Sse,
30}
31
32impl fmt::Display for Transport {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Stdio => write!(f, "stdio"),
36            Self::Http => write!(f, "http"),
37            Self::Sse => write!(f, "sse"),
38        }
39    }
40}
41
42/// Error returned when parsing an unknown transport string.
43#[derive(Debug, Clone, thiserror::Error)]
44#[error("unknown transport: {0}")]
45pub struct TransportParseError(pub String);
46
47impl FromStr for Transport {
48    type Err = TransportParseError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        match s {
52            "stdio" => Ok(Self::Stdio),
53            "http" => Ok(Self::Http),
54            "sse" => Ok(Self::Sse),
55            other => Err(TransportParseError(other.to_string())),
56        }
57    }
58}
59
60impl TryFrom<&str> for Transport {
61    type Error = TransportParseError;
62
63    fn try_from(s: &str) -> Result<Self, Self::Error> {
64        s.parse()
65    }
66}
67
68#[cfg(test)]
69mod transport_tests {
70    use super::*;
71
72    #[test]
73    fn from_str_accepts_known_variants() {
74        assert_eq!("stdio".parse::<Transport>().unwrap(), Transport::Stdio);
75        assert_eq!("http".parse::<Transport>().unwrap(), Transport::Http);
76        assert_eq!("sse".parse::<Transport>().unwrap(), Transport::Sse);
77    }
78
79    #[test]
80    fn from_str_returns_error_for_unknown() {
81        let err = "websocket".parse::<Transport>().unwrap_err();
82        assert!(err.to_string().contains("websocket"));
83    }
84
85    #[test]
86    fn try_from_str_returns_error_for_unknown() {
87        assert!(Transport::try_from("bogus").is_err());
88    }
89}
90
91/// Output format for `--output-format`.
92#[derive(Debug, Clone, Copy, Default)]
93pub enum OutputFormat {
94    /// Plain text output (default).
95    #[default]
96    Text,
97    /// Single JSON result object.
98    Json,
99    /// Streaming NDJSON.
100    StreamJson,
101}
102
103impl OutputFormat {
104    pub(crate) fn as_arg(&self) -> &'static str {
105        match self {
106            Self::Text => "text",
107            Self::Json => "json",
108            Self::StreamJson => "stream-json",
109        }
110    }
111}
112
113/// Permission mode for `--permission-mode`.
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub enum PermissionMode {
117    /// Default interactive permissions.
118    #[default]
119    Default,
120    /// Auto-accept file edits.
121    AcceptEdits,
122    /// Bypass all permission checks.
123    ///
124    /// **Deprecated.** Reaching for this variant directly puts a
125    /// bypass-mode query one keystroke away in any code path, which
126    /// is exactly the footgun the variant enables. Use
127    /// [`crate::dangerous::DangerousClient`] instead, which gates
128    /// construction on the `CLAUDE_WRAPPER_ALLOW_DANGEROUS` env-var
129    /// and makes the intent obvious at the call site. The variant
130    /// itself will stay available through the current major version
131    /// so existing callers keep compiling (with a deprecation
132    /// warning).
133    #[deprecated(
134        since = "0.5.1",
135        note = "use claude_wrapper::dangerous::DangerousClient instead; \
136                direct BypassPermissions usage is a footgun and will go \
137                away in a future major release"
138    )]
139    BypassPermissions,
140    /// Don't ask for permissions (deny by default).
141    DontAsk,
142    /// Plan mode (read-only).
143    Plan,
144    /// Auto mode.
145    Auto,
146}
147
148impl PermissionMode {
149    pub(crate) fn as_arg(&self) -> &'static str {
150        match self {
151            Self::Default => "default",
152            Self::AcceptEdits => "acceptEdits",
153            #[allow(deprecated)]
154            Self::BypassPermissions => "bypassPermissions",
155            Self::DontAsk => "dontAsk",
156            Self::Plan => "plan",
157            Self::Auto => "auto",
158        }
159    }
160}
161
162/// Input format for `--input-format`.
163#[derive(Debug, Clone, Copy, Default)]
164pub enum InputFormat {
165    /// Plain text input (default).
166    #[default]
167    Text,
168    /// Streaming JSON input.
169    StreamJson,
170}
171
172impl InputFormat {
173    pub(crate) fn as_arg(&self) -> &'static str {
174        match self {
175            Self::Text => "text",
176            Self::StreamJson => "stream-json",
177        }
178    }
179}
180
181/// Effort level for `--effort`.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "lowercase")]
184pub enum Effort {
185    /// Low effort.
186    Low,
187    /// Medium effort (default).
188    Medium,
189    /// High effort.
190    High,
191    /// Extra-high effort.
192    Xhigh,
193    /// Maximum effort, most thorough.
194    Max,
195}
196
197impl Effort {
198    pub(crate) fn as_arg(&self) -> &'static str {
199        match self {
200            Self::Low => "low",
201            Self::Medium => "medium",
202            Self::High => "high",
203            Self::Xhigh => "xhigh",
204            Self::Max => "max",
205        }
206    }
207}
208
209/// Scope for MCP and plugin commands.
210#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
211pub enum Scope {
212    /// Local scope (current directory).
213    #[default]
214    Local,
215    /// User scope (global).
216    User,
217    /// Project scope.
218    Project,
219    /// Managed scope -- plugins installed by an outer manager
220    /// rather than directly by the user. Accepted by `claude plugin
221    /// update --scope managed` as of CLI 2.1.143.
222    Managed,
223}
224
225impl Scope {
226    pub(crate) fn as_arg(&self) -> &'static str {
227        match self {
228            Self::Local => "local",
229            Self::User => "user",
230            Self::Project => "project",
231            Self::Managed => "managed",
232        }
233    }
234}
235
236/// Authentication status returned by `claude auth status --json`.
237///
238/// # Example
239///
240/// ```no_run
241/// # async fn example() -> claude_wrapper::Result<()> {
242/// let claude = claude_wrapper::Claude::builder().build()?;
243/// let status = claude_wrapper::AuthStatusCommand::new()
244///     .execute_json(&claude).await?;
245///
246/// if status.logged_in {
247///     println!("Logged in as {}", status.email.unwrap_or_default());
248/// }
249/// # Ok(())
250/// # }
251/// ```
252#[cfg(feature = "json")]
253#[derive(Debug, Clone, Deserialize, Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct AuthStatus {
256    /// Whether the user is currently logged in.
257    #[serde(default)]
258    pub logged_in: bool,
259    /// Authentication method (e.g. "claude.ai").
260    #[serde(default)]
261    pub auth_method: Option<String>,
262    /// API provider (e.g. "firstParty").
263    #[serde(default)]
264    pub api_provider: Option<String>,
265    /// Authenticated user's email address.
266    #[serde(default)]
267    pub email: Option<String>,
268    /// Organization ID.
269    #[serde(default)]
270    pub org_id: Option<String>,
271    /// Organization name.
272    #[serde(default)]
273    pub org_name: Option<String>,
274    /// Subscription type (e.g. "pro", "max").
275    #[serde(default)]
276    pub subscription_type: Option<String>,
277    /// Any additional fields not explicitly modeled.
278    #[serde(flatten)]
279    pub extra: std::collections::HashMap<String, serde_json::Value>,
280}
281
282/// A message from a query result, representing one turn in the conversation.
283#[cfg(feature = "json")]
284#[derive(Debug, Clone, Deserialize, Serialize)]
285pub struct QueryMessage {
286    /// The role of the message sender (e.g., "user", "assistant").
287    #[serde(default)]
288    pub role: String,
289    /// The text content of the message.
290    #[serde(default)]
291    pub content: serde_json::Value,
292    /// Additional fields returned by the CLI not captured in typed fields.
293    #[serde(flatten)]
294    pub extra: std::collections::HashMap<String, serde_json::Value>,
295}
296
297/// Result from a query with `--output-format json`.
298#[cfg(feature = "json")]
299#[derive(Debug, Clone, Deserialize, Serialize)]
300pub struct QueryResult {
301    /// The text content of the query response.
302    #[serde(default)]
303    pub result: String,
304    /// The session ID for continuing conversations.
305    #[serde(default)]
306    pub session_id: String,
307    /// Total cost of the query in USD.
308    #[serde(default, rename = "total_cost_usd", alias = "cost_usd")]
309    pub cost_usd: Option<f64>,
310    /// Duration of the query in milliseconds.
311    #[serde(default)]
312    pub duration_ms: Option<u64>,
313    /// Number of conversation turns in the query.
314    #[serde(default)]
315    pub num_turns: Option<u32>,
316    /// Whether the query resulted in an error.
317    #[serde(default)]
318    pub is_error: bool,
319    /// Additional fields returned by the CLI not captured in typed fields.
320    #[serde(flatten)]
321    pub extra: std::collections::HashMap<String, serde_json::Value>,
322}
323
324#[cfg(all(test, feature = "json"))]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn query_result_deserializes_total_cost_usd() {
330        let json =
331            r#"{"result":"hello","session_id":"s1","total_cost_usd":0.042,"is_error":false}"#;
332        let qr: QueryResult = serde_json::from_str(json).unwrap();
333        assert_eq!(qr.cost_usd, Some(0.042));
334    }
335
336    #[test]
337    fn query_result_deserializes_cost_usd_alias() {
338        let json = r#"{"result":"hello","session_id":"s1","cost_usd":0.01,"is_error":false}"#;
339        let qr: QueryResult = serde_json::from_str(json).unwrap();
340        assert_eq!(qr.cost_usd, Some(0.01));
341    }
342
343    #[test]
344    fn query_result_missing_cost_defaults_to_none() {
345        let json = r#"{"result":"hello","session_id":"s1","is_error":false}"#;
346        let qr: QueryResult = serde_json::from_str(json).unwrap();
347        assert_eq!(qr.cost_usd, None);
348    }
349
350    #[test]
351    fn query_result_from_stream_result_event() {
352        // Exact shape of a --output-format stream-json "result" event.
353        // The flattened `extra` must absorb type/subtype without
354        // breaking typed field parsing.
355        let json = r#"{"type":"result","subtype":"success","result":"streamed","session_id":"sess-1","total_cost_usd":0.03,"num_turns":1,"is_error":false}"#;
356        let qr: QueryResult = serde_json::from_str(json).unwrap();
357        assert_eq!(qr.cost_usd, Some(0.03));
358        assert_eq!(qr.num_turns, Some(1));
359        assert_eq!(qr.session_id, "sess-1");
360        assert_eq!(qr.result, "streamed");
361        assert_eq!(
362            qr.extra.get("type").and_then(|v| v.as_str()),
363            Some("result")
364        );
365    }
366
367    #[test]
368    fn query_result_deserializes_num_turns() {
369        let json = r#"{"result":"done","session_id":"s2","total_cost_usd":0.1,"num_turns":5,"is_error":false}"#;
370        let qr: QueryResult = serde_json::from_str(json).unwrap();
371        assert_eq!(qr.num_turns, Some(5));
372        assert_eq!(qr.cost_usd, Some(0.1));
373    }
374
375    #[test]
376    fn query_result_serializes_as_total_cost_usd() {
377        let qr = QueryResult {
378            result: "ok".into(),
379            session_id: "s1".into(),
380            cost_usd: Some(0.05),
381            duration_ms: None,
382            num_turns: Some(3),
383            is_error: false,
384            extra: Default::default(),
385        };
386        let json = serde_json::to_string(&qr).unwrap();
387        assert!(json.contains("\"total_cost_usd\""));
388        assert!(json.contains("\"num_turns\""));
389    }
390}