Skip to main content

apiari_claude_sdk/
session.rs

1//! Session configuration and CLI argument building.
2//!
3//! [`SessionOptions`] holds all the knobs for launching a Claude session:
4//! model, system prompt, working directory, allowed tools, resume/continue
5//! flags, and so on.  Its [`to_cli_args`](SessionOptions::to_cli_args)
6//! method converts the options into `claude` CLI flags.
7
8use std::path::PathBuf;
9
10/// Options for creating a new Claude session.
11///
12/// These map 1-to-1 onto `claude` CLI flags. Only the fields you set will
13/// produce flags; `None` / empty-vec fields are omitted.
14#[derive(Debug, Clone, Default)]
15pub struct SessionOptions {
16    // -- Session identity --------------------------------------------------
17    /// Resume a specific session by ID.
18    pub resume: Option<String>,
19
20    /// Continue the most recent conversation in the working directory.
21    pub continue_conversation: bool,
22
23    /// When resuming, fork to a new session ID.
24    pub fork_session: bool,
25
26    /// Use a specific session ID (must be a valid UUID).
27    pub session_id: Option<String>,
28
29    // -- Model / budget ----------------------------------------------------
30    /// Model to use (e.g. `"sonnet"`, `"opus"`, or full model name).
31    pub model: Option<String>,
32
33    /// Fallback model when the primary is overloaded.
34    pub fallback_model: Option<String>,
35
36    /// Maximum dollar amount to spend on API calls.
37    pub max_budget_usd: Option<f64>,
38
39    /// Maximum number of agentic turns.
40    pub max_turns: Option<u64>,
41
42    // -- System prompt -----------------------------------------------------
43    /// Replace the entire system prompt.
44    pub system_prompt: Option<String>,
45
46    /// Append text to the default system prompt.
47    pub append_system_prompt: Option<String>,
48
49    // -- Tools & permissions -----------------------------------------------
50    /// Restrict which built-in tools Claude can use (e.g. `["Bash", "Edit", "Read"]`).
51    pub tools: Vec<String>,
52
53    /// Tools that execute without prompting for permission.
54    pub allowed_tools: Vec<String>,
55
56    /// Tools that are explicitly denied.
57    pub disallowed_tools: Vec<String>,
58
59    /// Permission mode.
60    pub permission_mode: Option<PermissionMode>,
61
62    /// Skip all permission checks (dangerous!).
63    pub dangerously_skip_permissions: bool,
64
65    // -- MCP ---------------------------------------------------------------
66    /// Path(s) to MCP config JSON files.
67    pub mcp_config: Vec<String>,
68
69    /// Only use MCP servers from `mcp_config`, ignoring all other configs.
70    pub strict_mcp_config: bool,
71
72    // -- Working directory & extra dirs ------------------------------------
73    /// Working directory for the Claude session.
74    pub working_dir: Option<PathBuf>,
75
76    /// Additional directories to allow tool access to.
77    pub add_dirs: Vec<PathBuf>,
78
79    // -- Streaming ---------------------------------------------------------
80    /// Include partial streaming events in output.
81    pub include_partial_messages: bool,
82
83    // -- Other -------------------------------------------------------------
84    /// Effort level (`"low"`, `"medium"`, `"high"`).
85    pub effort: Option<String>,
86
87    /// Disable session persistence.
88    pub no_session_persistence: bool,
89
90    /// JSON schema for structured output validation.
91    pub json_schema: Option<String>,
92
93    /// Custom subagents defined as a JSON string.
94    pub agents: Option<String>,
95
96    /// Custom settings file or JSON string.
97    pub settings: Option<String>,
98
99    /// Setting sources to load.
100    pub setting_sources: Vec<String>,
101
102    /// Additional environment variables to set.
103    pub env_vars: Vec<(String, String)>,
104}
105
106/// Permission modes for controlling tool execution.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum PermissionMode {
109    /// Standard permission behavior.
110    Default,
111    /// Auto-accept file edits.
112    AcceptEdits,
113    /// Planning mode (no execution).
114    Plan,
115    /// Bypass all permission checks.
116    BypassPermissions,
117    /// Ask for permission (do not ask).
118    DontAsk,
119}
120
121impl PermissionMode {
122    /// Return the CLI flag value for this mode.
123    pub fn as_str(&self) -> &'static str {
124        match self {
125            Self::Default => "default",
126            Self::AcceptEdits => "acceptEdits",
127            Self::Plan => "plan",
128            Self::BypassPermissions => "bypassPermissions",
129            Self::DontAsk => "dontAsk",
130        }
131    }
132}
133
134impl SessionOptions {
135    /// Convert these options into CLI arguments for the `claude` binary.
136    ///
137    /// This does **not** include the base arguments (`--print`,
138    /// `--output-format stream-json`, etc.) — those are added by
139    /// [`Transport::spawn`](crate::transport::Transport::spawn).
140    pub fn to_cli_args(&self) -> Vec<String> {
141        let mut args = Vec::new();
142
143        // Session identity.
144        if let Some(ref id) = self.resume {
145            args.extend(["--resume".to_owned(), id.clone()]);
146        }
147        if self.continue_conversation {
148            args.push("--continue".to_owned());
149        }
150        if self.fork_session {
151            args.push("--fork-session".to_owned());
152        }
153        if let Some(ref id) = self.session_id {
154            args.extend(["--session-id".to_owned(), id.clone()]);
155        }
156
157        // Model / budget.
158        if let Some(ref model) = self.model {
159            args.extend(["--model".to_owned(), model.clone()]);
160        }
161        if let Some(ref model) = self.fallback_model {
162            args.extend(["--fallback-model".to_owned(), model.clone()]);
163        }
164        if let Some(budget) = self.max_budget_usd {
165            args.extend(["--max-budget-usd".to_owned(), budget.to_string()]);
166        }
167        if let Some(turns) = self.max_turns {
168            args.extend(["--max-turns".to_owned(), turns.to_string()]);
169        }
170
171        // System prompt.
172        if let Some(ref prompt) = self.system_prompt {
173            args.extend(["--system-prompt".to_owned(), prompt.clone()]);
174        }
175        if let Some(ref prompt) = self.append_system_prompt {
176            args.extend(["--append-system-prompt".to_owned(), prompt.clone()]);
177        }
178
179        // Tools & permissions.
180        if !self.tools.is_empty() {
181            args.extend(["--tools".to_owned(), self.tools.join(",")]);
182        }
183        if !self.allowed_tools.is_empty() {
184            args.push("--allowedTools".to_owned());
185            for tool in &self.allowed_tools {
186                args.push(tool.clone());
187            }
188        }
189        if !self.disallowed_tools.is_empty() {
190            args.push("--disallowedTools".to_owned());
191            for tool in &self.disallowed_tools {
192                args.push(tool.clone());
193            }
194        }
195        if let Some(mode) = self.permission_mode {
196            args.extend(["--permission-mode".to_owned(), mode.as_str().to_owned()]);
197        }
198        if self.dangerously_skip_permissions {
199            args.push("--dangerously-skip-permissions".to_owned());
200        }
201
202        // MCP.
203        if !self.mcp_config.is_empty() {
204            args.push("--mcp-config".to_owned());
205            for cfg in &self.mcp_config {
206                args.push(cfg.clone());
207            }
208        }
209        if self.strict_mcp_config {
210            args.push("--strict-mcp-config".to_owned());
211        }
212
213        // Additional directories.
214        if !self.add_dirs.is_empty() {
215            args.push("--add-dir".to_owned());
216            for dir in &self.add_dirs {
217                args.push(dir.display().to_string());
218            }
219        }
220
221        // Streaming.
222        if self.include_partial_messages {
223            args.push("--include-partial-messages".to_owned());
224        }
225
226        // Other.
227        if let Some(ref effort) = self.effort {
228            args.extend(["--effort".to_owned(), effort.clone()]);
229        }
230        if self.no_session_persistence {
231            args.push("--no-session-persistence".to_owned());
232        }
233        if let Some(ref schema) = self.json_schema {
234            args.extend(["--json-schema".to_owned(), schema.clone()]);
235        }
236        if let Some(ref agents) = self.agents {
237            args.extend(["--agents".to_owned(), agents.clone()]);
238        }
239        if let Some(ref settings) = self.settings {
240            args.extend(["--settings".to_owned(), settings.clone()]);
241        }
242        if !self.setting_sources.is_empty() {
243            args.extend([
244                "--setting-sources".to_owned(),
245                self.setting_sources.join(","),
246            ]);
247        }
248
249        args
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn empty_options_produce_no_args() {
259        let opts = SessionOptions::default();
260        assert!(opts.to_cli_args().is_empty());
261    }
262
263    #[test]
264    fn model_and_tools_args() {
265        let opts = SessionOptions {
266            model: Some("sonnet".to_owned()),
267            allowed_tools: vec!["Bash".to_owned(), "Read".to_owned()],
268            max_turns: Some(5),
269            ..Default::default()
270        };
271        let args = opts.to_cli_args();
272        assert!(args.contains(&"--model".to_owned()));
273        assert!(args.contains(&"sonnet".to_owned()));
274        assert!(args.contains(&"--allowedTools".to_owned()));
275        assert!(args.contains(&"Bash".to_owned()));
276        assert!(args.contains(&"Read".to_owned()));
277        assert!(args.contains(&"--max-turns".to_owned()));
278        assert!(args.contains(&"5".to_owned()));
279    }
280
281    #[test]
282    fn resume_and_continue_flags() {
283        let opts = SessionOptions {
284            resume: Some("abc-123".to_owned()),
285            fork_session: true,
286            ..Default::default()
287        };
288        let args = opts.to_cli_args();
289        assert!(args.contains(&"--resume".to_owned()));
290        assert!(args.contains(&"abc-123".to_owned()));
291        assert!(args.contains(&"--fork-session".to_owned()));
292    }
293
294    #[test]
295    fn permission_mode_flag() {
296        let opts = SessionOptions {
297            permission_mode: Some(PermissionMode::AcceptEdits),
298            ..Default::default()
299        };
300        let args = opts.to_cli_args();
301        assert!(args.contains(&"--permission-mode".to_owned()));
302        assert!(args.contains(&"acceptEdits".to_owned()));
303    }
304}