Skip to main content

claude_wrapper/
types.rs

1//! Shared enums and serde types used across the command builders.
2//!
3//! Small value types like [`Transport`], [`Scope`], [`Effort`],
4//! [`PermissionMode`], [`OutputFormat`], and [`InputFormat`] that map
5//! onto `claude` CLI flag values, with `Display`/`FromStr` and serde
6//! wiring so they round-trip cleanly through args and JSON.
7
8use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13/// Transport type for MCP server connections.
14///
15/// # Example
16///
17/// ```
18/// use claude_wrapper::Transport;
19/// use std::str::FromStr;
20///
21/// let t = Transport::from_str("stdio").unwrap();
22/// assert_eq!(t, Transport::Stdio);
23/// assert_eq!(t.to_string(), "stdio");
24///
25/// let t: Transport = "http".parse().unwrap();
26/// assert_eq!(t, Transport::Http);
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Transport {
31    /// Standard I/O transport — server runs as a subprocess.
32    Stdio,
33    /// HTTP transport — server accessible via URL.
34    Http,
35    /// Server-Sent Events transport — server accessible via URL with SSE.
36    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/// Error returned when parsing an unknown transport string.
50#[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/// Output format for `--output-format`.
99#[derive(Debug, Clone, Copy, Default)]
100pub enum OutputFormat {
101    /// Plain text output (default).
102    #[default]
103    Text,
104    /// Single JSON result object.
105    Json,
106    /// Streaming NDJSON.
107    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/// Permission mode for `--permission-mode`.
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub enum PermissionMode {
124    /// Default interactive permissions.
125    #[default]
126    Default,
127    /// Auto-accept file edits.
128    AcceptEdits,
129    /// Bypass all permission checks.
130    ///
131    /// **Deprecated.** Reaching for this variant directly puts a
132    /// bypass-mode query one keystroke away in any code path, which
133    /// is exactly the footgun the variant enables. Use
134    /// [`crate::dangerous::DangerousClient`] instead, which gates
135    /// construction on the `CLAUDE_WRAPPER_ALLOW_DANGEROUS` env-var
136    /// and makes the intent obvious at the call site. The variant
137    /// itself will stay available through the current major version
138    /// so existing callers keep compiling (with a deprecation
139    /// warning).
140    #[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    /// Don't ask for permissions (deny by default).
148    DontAsk,
149    /// Plan mode (read-only).
150    Plan,
151    /// Auto mode.
152    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/// Input format for `--input-format`.
170#[derive(Debug, Clone, Copy, Default)]
171pub enum InputFormat {
172    /// Plain text input (default).
173    #[default]
174    Text,
175    /// Streaming JSON input.
176    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/// Effort level for `--effort`.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "lowercase")]
191pub enum Effort {
192    /// Low effort.
193    Low,
194    /// Medium effort (default).
195    Medium,
196    /// High effort.
197    High,
198    /// Extra-high effort.
199    Xhigh,
200    /// Maximum effort, most thorough.
201    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/// How far a hermetic seal reaches when sealing the ambient `~/.claude`
217/// promptspace (`CLAUDE.md`, agents, skills, MCP servers).
218///
219/// Chooses the value passed to `--setting-sources`; the seal itself is
220/// applied by `hermetic` / `hermetic_scoped` on
221/// [`QueryCommand`](crate::QueryCommand) and
222/// [`DuplexOptions`](crate::duplex::DuplexOptions), which also set
223/// `--strict-mcp-config` and
224/// `--exclude-dynamic-system-prompt-sections`.
225///
226/// A hermetic seal never touches authentication. It is distinct from
227/// `--bare`, which forces API-key billing by reading auth strictly from
228/// `ANTHROPIC_API_KEY` / `apiKeyHelper`; a seal leaves OAuth and
229/// keychain auth working as normal.
230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
231pub enum HermeticScope {
232    /// Drop every ambient setting source: user, project, and local
233    /// (`--setting-sources ""`). Only the CLI's built-in agents and
234    /// defaults remain; a user-level `~/.claude` no longer leaks in.
235    #[default]
236    Full,
237    /// Seal only the project and local ambient config while keeping the
238    /// user's global `~/.claude` (`--setting-sources user`). A project
239    /// `CLAUDE.md` and project-level agents stop being adopted.
240    Project,
241}
242
243impl HermeticScope {
244    /// The value emitted for `--setting-sources` under this scope.
245    pub(crate) fn setting_sources_value(self) -> &'static str {
246        match self {
247            Self::Full => "",
248            Self::Project => "user",
249        }
250    }
251}
252
253/// Scope for MCP and plugin commands.
254#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
255pub enum Scope {
256    /// Local scope (current directory).
257    #[default]
258    Local,
259    /// User scope (global).
260    User,
261    /// Project scope.
262    Project,
263    /// Managed scope -- plugins installed by an outer manager
264    /// rather than directly by the user. Accepted by `claude plugin
265    /// update --scope managed` as of CLI 2.1.143.
266    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/// Authentication status returned by `claude auth status --json`.
281///
282/// # Example
283///
284/// ```no_run
285/// # async fn example() -> claude_wrapper::Result<()> {
286/// let claude = claude_wrapper::Claude::builder().build()?;
287/// let status = claude_wrapper::AuthStatusCommand::new()
288///     .execute_json(&claude).await?;
289///
290/// if status.logged_in {
291///     println!("Logged in as {}", status.email.unwrap_or_default());
292/// }
293/// # Ok(())
294/// # }
295/// ```
296#[cfg(feature = "json")]
297#[derive(Debug, Clone, Deserialize, Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct AuthStatus {
300    /// Whether the user is currently logged in.
301    #[serde(default)]
302    pub logged_in: bool,
303    /// Authentication method (e.g. "claude.ai").
304    #[serde(default)]
305    pub auth_method: Option<String>,
306    /// API provider (e.g. "firstParty").
307    #[serde(default)]
308    pub api_provider: Option<String>,
309    /// Authenticated user's email address.
310    #[serde(default)]
311    pub email: Option<String>,
312    /// Organization ID.
313    #[serde(default)]
314    pub org_id: Option<String>,
315    /// Organization name.
316    #[serde(default)]
317    pub org_name: Option<String>,
318    /// Subscription type (e.g. "pro", "max").
319    #[serde(default)]
320    pub subscription_type: Option<String>,
321    /// Any additional fields not explicitly modeled.
322    #[serde(flatten)]
323    pub extra: std::collections::HashMap<String, serde_json::Value>,
324}
325
326/// A message from a query result, representing one turn in the conversation.
327#[cfg(feature = "json")]
328#[derive(Debug, Clone, Deserialize, Serialize)]
329pub struct QueryMessage {
330    /// The role of the message sender (e.g., "user", "assistant").
331    #[serde(default)]
332    pub role: String,
333    /// The text content of the message.
334    #[serde(default)]
335    pub content: serde_json::Value,
336    /// Additional fields returned by the CLI not captured in typed fields.
337    #[serde(flatten)]
338    pub extra: std::collections::HashMap<String, serde_json::Value>,
339}
340
341/// Result from a query with `--output-format json`.
342#[cfg(feature = "json")]
343#[derive(Debug, Clone, Deserialize, Serialize)]
344pub struct QueryResult {
345    /// The text content of the query response.
346    #[serde(default)]
347    pub result: String,
348    /// The session ID for continuing conversations.
349    #[serde(default)]
350    pub session_id: String,
351    /// Total cost of the query in USD.
352    #[serde(default, rename = "total_cost_usd", alias = "cost_usd")]
353    pub cost_usd: Option<f64>,
354    /// Duration of the query in milliseconds.
355    #[serde(default)]
356    pub duration_ms: Option<u64>,
357    /// Number of conversation turns in the query.
358    #[serde(default)]
359    pub num_turns: Option<u32>,
360    /// Whether the query resulted in an error.
361    #[serde(default)]
362    pub is_error: bool,
363    /// Additional fields returned by the CLI not captured in typed fields.
364    #[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        // Exact shape of a --output-format stream-json "result" event.
397        // The flattened `extra` must absorb type/subtype without
398        // breaking typed field parsing.
399        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}