Skip to main content

claude_wrapper/
lib.rs

1//! A type-safe Claude Code CLI wrapper for Rust.
2//!
3//! `claude-wrapper` provides a builder-pattern interface for invoking the
4//! `claude` CLI programmatically. It follows the same design philosophy as
5//! [`docker-wrapper`](https://crates.io/crates/docker-wrapper) and
6//! [`terraform-wrapper`](https://crates.io/crates/terraform-wrapper):
7//! each CLI subcommand is a builder struct that produces typed output.
8//!
9//! # Quick Start
10//!
11//! ```no_run
12//! use claude_wrapper::{Claude, ClaudeCommand, QueryCommand, OutputFormat};
13//!
14//! # async fn example() -> claude_wrapper::Result<()> {
15//! let claude = Claude::builder().build()?;
16//!
17//! // Simple oneshot query
18//! let output = QueryCommand::new("explain this error: file not found")
19//!     .model("sonnet")
20//!     .output_format(OutputFormat::Json)
21//!     .execute(&claude)
22//!     .await?;
23//!
24//! println!("{}", output.stdout);
25//! # Ok(())
26//! # }
27//! ```
28//!
29//! # MCP Config Generation
30//!
31//! ```no_run
32//! use claude_wrapper::McpConfigBuilder;
33//!
34//! # fn example() -> claude_wrapper::Result<()> {
35//! let config = McpConfigBuilder::new()
36//!     .http_server("my-hub", "http://127.0.0.1:9090")
37//!     .write_to("/tmp/my-project/.mcp.json")?;
38//! # Ok(())
39//! # }
40//! ```
41
42pub mod command;
43pub mod error;
44pub mod exec;
45pub mod mcp_config;
46pub mod streaming;
47pub mod types;
48
49use std::collections::HashMap;
50use std::path::{Path, PathBuf};
51use std::time::Duration;
52
53pub use command::ClaudeCommand;
54pub use command::agents::AgentsCommand;
55pub use command::auth::AuthStatusCommand;
56pub use command::doctor::DoctorCommand;
57pub use command::marketplace::{
58    MarketplaceAddCommand, MarketplaceListCommand, MarketplaceRemoveCommand,
59    MarketplaceUpdateCommand,
60};
61pub use command::mcp::{
62    McpAddCommand, McpAddFromDesktopCommand, McpAddJsonCommand, McpGetCommand, McpListCommand,
63    McpRemoveCommand, McpResetProjectChoicesCommand,
64};
65pub use command::plugin::{
66    PluginDisableCommand, PluginEnableCommand, PluginInstallCommand, PluginListCommand,
67    PluginUninstallCommand, PluginUpdateCommand, PluginValidateCommand,
68};
69pub use command::query::QueryCommand;
70pub use command::raw::RawCommand;
71pub use command::version::VersionCommand;
72pub use error::{Error, Result};
73pub use exec::CommandOutput;
74pub use mcp_config::{McpConfigBuilder, McpServerConfig};
75pub use types::*;
76
77/// The Claude CLI client. Holds shared configuration applied to all commands.
78///
79/// Create one via [`Claude::builder()`] and reuse it across commands.
80#[derive(Debug, Clone)]
81pub struct Claude {
82    pub(crate) binary: PathBuf,
83    pub(crate) working_dir: Option<PathBuf>,
84    pub(crate) env: HashMap<String, String>,
85    pub(crate) global_args: Vec<String>,
86    pub(crate) timeout: Option<Duration>,
87}
88
89impl Claude {
90    /// Create a new builder for configuring the Claude client.
91    #[must_use]
92    pub fn builder() -> ClaudeBuilder {
93        ClaudeBuilder::default()
94    }
95
96    /// Get the path to the claude binary.
97    #[must_use]
98    pub fn binary(&self) -> &Path {
99        &self.binary
100    }
101
102    /// Get the working directory, if set.
103    #[must_use]
104    pub fn working_dir(&self) -> Option<&Path> {
105        self.working_dir.as_deref()
106    }
107
108    /// Create a clone of this client with a different working directory.
109    #[must_use]
110    pub fn with_working_dir(&self, dir: impl Into<PathBuf>) -> Self {
111        let mut clone = self.clone();
112        clone.working_dir = Some(dir.into());
113        clone
114    }
115}
116
117/// Builder for creating a [`Claude`] client.
118///
119/// # Example
120///
121/// ```no_run
122/// use claude_wrapper::Claude;
123///
124/// # fn example() -> claude_wrapper::Result<()> {
125/// let claude = Claude::builder()
126///     .env("AWS_REGION", "us-west-2")
127///     .timeout_secs(120)
128///     .build()?;
129/// # Ok(())
130/// # }
131/// ```
132#[derive(Debug, Default)]
133pub struct ClaudeBuilder {
134    binary: Option<PathBuf>,
135    working_dir: Option<PathBuf>,
136    env: HashMap<String, String>,
137    global_args: Vec<String>,
138    timeout: Option<Duration>,
139}
140
141impl ClaudeBuilder {
142    /// Set the path to the claude binary.
143    ///
144    /// If not set, the binary is resolved from PATH using `which`.
145    #[must_use]
146    pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
147        self.binary = Some(path.into());
148        self
149    }
150
151    /// Set the working directory for all commands.
152    ///
153    /// The spawned process will use this as its current directory.
154    #[must_use]
155    pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
156        self.working_dir = Some(path.into());
157        self
158    }
159
160    /// Add an environment variable to pass to all commands.
161    #[must_use]
162    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
163        self.env.insert(key.into(), value.into());
164        self
165    }
166
167    /// Add multiple environment variables.
168    #[must_use]
169    pub fn envs(
170        mut self,
171        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
172    ) -> Self {
173        for (k, v) in vars {
174            self.env.insert(k.into(), v.into());
175        }
176        self
177    }
178
179    /// Set a default timeout for all commands (in seconds).
180    #[must_use]
181    pub fn timeout_secs(mut self, seconds: u64) -> Self {
182        self.timeout = Some(Duration::from_secs(seconds));
183        self
184    }
185
186    /// Set a default timeout for all commands.
187    #[must_use]
188    pub fn timeout(mut self, duration: Duration) -> Self {
189        self.timeout = Some(duration);
190        self
191    }
192
193    /// Add a global argument applied to all commands.
194    ///
195    /// This is an escape hatch for flags not yet covered by the API.
196    #[must_use]
197    pub fn arg(mut self, arg: impl Into<String>) -> Self {
198        self.global_args.push(arg.into());
199        self
200    }
201
202    /// Build the Claude client, resolving the binary path.
203    pub fn build(self) -> Result<Claude> {
204        let binary = match self.binary {
205            Some(path) => path,
206            None => which::which("claude").map_err(|_| Error::NotFound)?,
207        };
208
209        Ok(Claude {
210            binary,
211            working_dir: self.working_dir,
212            env: self.env,
213            global_args: self.global_args,
214            timeout: self.timeout,
215        })
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_builder_with_binary() {
225        let claude = Claude::builder()
226            .binary("/usr/local/bin/claude")
227            .env("FOO", "bar")
228            .timeout_secs(60)
229            .build()
230            .unwrap();
231
232        assert_eq!(claude.binary, PathBuf::from("/usr/local/bin/claude"));
233        assert_eq!(claude.env.get("FOO").unwrap(), "bar");
234        assert_eq!(claude.timeout, Some(Duration::from_secs(60)));
235    }
236
237    #[test]
238    fn test_builder_global_args() {
239        let claude = Claude::builder()
240            .binary("/usr/local/bin/claude")
241            .arg("--verbose")
242            .build()
243            .unwrap();
244
245        assert_eq!(claude.global_args, vec!["--verbose"]);
246    }
247}