claude_agent/agent/options/
cli.rs

1//! CLI integration methods for AgentBuilder.
2//!
3//! Resource loading follows a fixed order regardless of method call sequence:
4//! Enterprise → User → Project → Local (later levels override earlier).
5
6use std::path::Path;
7
8use crate::auth::Auth;
9use crate::common::{IndexRegistry, Named, Provider};
10use crate::config::{Settings, SettingsLoader};
11use crate::context::{LeveledMemoryProvider, MemoryLoader, enterprise_base_path, user_base_path};
12use crate::hooks::CommandHook;
13use crate::output_style::file_output_style_provider;
14use crate::permissions::{PermissionMode, PermissionPolicy};
15use crate::skills::SkillIndexLoader;
16use crate::subagents::{SubagentIndexLoader, builtin_subagents};
17
18use super::builder::AgentBuilder;
19
20impl AgentBuilder {
21    /// Initializes Claude Code CLI authentication and working directory.
22    ///
23    /// This is the minimal setup. Use `with_*_resources()` methods to enable
24    /// loading from specific levels. Resources are loaded during `build()` in
25    /// a fixed order: Enterprise → User → Project → Local.
26    ///
27    /// # Example
28    ///
29    /// ```rust,no_run
30    /// # use claude_agent::Agent;
31    /// # async fn example() -> claude_agent::Result<()> {
32    /// let agent = Agent::builder()
33    ///     .from_claude_code("./project").await?
34    ///     .with_user_resources()
35    ///     .with_project_resources()
36    ///     .build()
37    ///     .await?;
38    /// # Ok(())
39    /// # }
40    /// ```
41    pub async fn from_claude_code(mut self, path: impl AsRef<Path>) -> crate::Result<Self> {
42        let path = path.as_ref();
43        self = self.auth(Auth::ClaudeCli).await?;
44        self.config.working_dir = Some(path.to_path_buf());
45        Ok(self)
46    }
47
48    /// Enables loading of enterprise-level resources during build.
49    ///
50    /// Resources are loaded from system configuration paths:
51    /// - macOS: `/Library/Application Support/ClaudeCode/`
52    /// - Linux: `/etc/claude-code/`
53    ///
54    /// This method only sets a flag; actual loading happens during `build()`
55    /// in the fixed order: Enterprise → User → Project → Local.
56    pub fn with_enterprise_resources(mut self) -> Self {
57        self.load_enterprise = true;
58        self
59    }
60
61    /// Enables loading of user-level resources during build.
62    ///
63    /// Resources are loaded from `~/.claude/`.
64    ///
65    /// This method only sets a flag; actual loading happens during `build()`
66    /// in the fixed order: Enterprise → User → Project → Local.
67    pub fn with_user_resources(mut self) -> Self {
68        self.load_user = true;
69        self
70    }
71
72    /// Enables loading of project-level resources during build.
73    ///
74    /// Resources are loaded from `{working_dir}/.claude/`.
75    /// Requires `from_claude_code()` to be called first to set the working directory.
76    ///
77    /// This method only sets a flag; actual loading happens during `build()`
78    /// in the fixed order: Enterprise → User → Project → Local.
79    pub fn with_project_resources(mut self) -> Self {
80        self.load_project = true;
81        self
82    }
83
84    /// Enables loading of local-level resources during build.
85    ///
86    /// Loads CLAUDE.local.md and settings.local.json from the project directory.
87    /// These are typically gitignored and contain personal/machine-specific settings.
88    /// Requires `from_claude_code()` to be called first to set the working directory.
89    ///
90    /// This method only sets a flag; actual loading happens during `build()`
91    /// in the fixed order: Enterprise → User → Project → Local.
92    pub fn with_local_resources(mut self) -> Self {
93        self.load_local = true;
94        self
95    }
96
97    // =========================================================================
98    // Internal resource loading methods (called by build.rs in fixed order)
99    // =========================================================================
100
101    pub(super) async fn load_enterprise_resources(&mut self) {
102        let Some(base) = enterprise_base_path() else {
103            return;
104        };
105
106        self.load_settings_from(&base).await;
107        self.load_skills_from(&base).await;
108        self.load_subagents_from(&base).await;
109        self.load_output_styles_from(&base).await;
110        self.load_memory_from(&base).await;
111    }
112
113    pub(super) async fn load_user_resources(&mut self) {
114        let Some(base) = user_base_path() else {
115            return;
116        };
117
118        self.load_settings_from(&base).await;
119        self.load_skills_from(&base).await;
120        self.load_subagents_from(&base).await;
121        self.load_output_styles_from(&base).await;
122        self.load_memory_from(&base).await;
123    }
124
125    pub(super) async fn load_project_resources(&mut self) {
126        let Some(working_dir) = self.config.working_dir.clone() else {
127            tracing::warn!("working_dir not set, call from_claude_code() first");
128            return;
129        };
130
131        self.load_settings_from(&working_dir).await;
132        self.load_skills_from(&working_dir).await;
133        self.load_subagents_from(&working_dir).await;
134        self.load_output_styles_from(&working_dir).await;
135        self.load_memory_from(&working_dir).await;
136    }
137
138    pub(super) async fn load_local_resources(&mut self) {
139        let Some(working_dir) = self.config.working_dir.clone() else {
140            tracing::warn!("working_dir not set, call from_claude_code() first");
141            return;
142        };
143
144        let mut settings_loader = SettingsLoader::new();
145        if settings_loader.load_local(&working_dir).await.is_ok() {
146            let settings = settings_loader.into_settings();
147            self.apply_settings_mut(&settings);
148        }
149
150        let loader = MemoryLoader::new();
151        if let Ok(content) = loader.load_local(&working_dir).await
152            && !content.local_md.is_empty()
153        {
154            let provider = self
155                .memory_provider
156                .get_or_insert_with(LeveledMemoryProvider::new);
157            provider.add_memory_content(content);
158        }
159    }
160
161    // =========================================================================
162    // Helper methods for resource loading
163    // =========================================================================
164
165    async fn load_settings_from(&mut self, base: &Path) {
166        let mut loader = SettingsLoader::new();
167        if loader.load_from(base).await.is_ok() {
168            let settings = loader.into_settings();
169            self.apply_settings_mut(&settings);
170        }
171    }
172
173    async fn load_skills_from(&mut self, base: &Path) {
174        let skills_dir = base.join(".claude").join("skills");
175        let loader = SkillIndexLoader::new();
176        if let Ok(skills) = loader.scan_directory(&skills_dir).await {
177            let count = skills.len();
178            for skill in skills {
179                tracing::debug!(skill_name = %skill.name, "Registering skill to builder");
180                self.skill_registry
181                    .get_or_insert_with(IndexRegistry::new)
182                    .register(skill);
183            }
184            tracing::info!(skill_count = count, "Skills loaded from project");
185        }
186    }
187
188    async fn load_subagents_from(&mut self, base: &Path) {
189        let loader = SubagentIndexLoader::new();
190        let subagents_dir = base.join(".claude").join("subagents");
191        if let Ok(subagents) = loader.scan_directory(&subagents_dir).await {
192            for subagent in subagents {
193                self.subagent_registry
194                    .get_or_insert_with(|| {
195                        let mut registry = IndexRegistry::new();
196                        registry.register_all(builtin_subagents());
197                        registry
198                    })
199                    .register(subagent);
200            }
201        }
202    }
203
204    async fn load_output_styles_from(&mut self, base: &Path) {
205        let provider = file_output_style_provider().with_project_path(base);
206        if let Ok(styles) = provider.load_all().await
207            && let Some(first) = styles.first()
208            && self.output_style_name.is_none()
209        {
210            self.output_style_name = Some(first.name().to_string());
211        }
212    }
213
214    async fn load_memory_from(&mut self, base: &Path) {
215        let loader = MemoryLoader::new();
216        if let Ok(content) = loader.load_shared(base).await
217            && !content.is_empty()
218        {
219            let provider = self
220                .memory_provider
221                .get_or_insert_with(LeveledMemoryProvider::new);
222            provider.add_memory_content(content);
223        }
224    }
225
226    /// Apply settings mutably (for internal use by load methods).
227    fn apply_settings_mut(&mut self, settings: &Settings) {
228        if let Some(model) = &settings.model {
229            self.config.model.primary = model.clone();
230        }
231        if let Some(small) = &settings.small_model {
232            self.config.model.small = small.clone();
233        }
234        if let Some(max_tokens) = settings.max_tokens {
235            self.config.model.max_tokens = max_tokens;
236        }
237
238        if let Some(ref hooks_settings) = settings.hooks {
239            for hook in CommandHook::from_settings(hooks_settings) {
240                self.hooks.register(hook);
241            }
242        }
243
244        self.config.security.env.extend(settings.env.clone());
245
246        if !settings.permissions.is_empty() {
247            let loaded_policy = settings.permissions.to_policy();
248            let existing_policy = std::mem::take(&mut self.config.security.permission_policy);
249            self.config.security.permission_policy =
250                Self::merge_permission_policies(existing_policy, loaded_policy);
251        }
252
253        if settings.sandbox.is_enabled() || settings.sandbox.has_network_settings() {
254            self.sandbox_settings = Some(settings.sandbox.clone());
255        }
256
257        if let Some(ref style_name) = settings.output_style {
258            self.output_style_name = Some(style_name.clone());
259        }
260
261        for (name, config_value) in &settings.mcp_servers {
262            if let Ok(config) = serde_json::from_value(config_value.clone()) {
263                self.mcp_configs.insert(name.clone(), config);
264            }
265        }
266
267        // Apply tool search settings
268        if !settings.tool_search.is_empty() && settings.tool_search.is_enabled() {
269            let context_window =
270                crate::types::context_window::for_model(&self.config.model.primary) as usize;
271            let config = settings.tool_search.to_config(context_window);
272            self.tool_search_config = Some(config);
273        }
274    }
275
276    /// Apply settings (for test use, returns Self for chaining).
277    #[cfg(test)]
278    pub(super) fn apply_settings(mut self, settings: Settings) -> Self {
279        self.apply_settings_mut(&settings);
280        self
281    }
282
283    fn merge_permission_policies(
284        from_settings: PermissionPolicy,
285        programmatic: PermissionPolicy,
286    ) -> PermissionPolicy {
287        let mode = if programmatic.mode != PermissionMode::Default {
288            programmatic.mode
289        } else {
290            from_settings.mode
291        };
292
293        let mut rules = from_settings.rules;
294        rules.extend(programmatic.rules);
295
296        let mut tool_limits = from_settings.tool_limits;
297        tool_limits.extend(programmatic.tool_limits);
298
299        PermissionPolicy {
300            mode,
301            rules,
302            tool_limits,
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_settings_apply_values() {
313        let settings = Settings {
314            model: Some("settings-model".to_string()),
315            small_model: Some("settings-small".to_string()),
316            max_tokens: Some(1000),
317            ..Default::default()
318        };
319
320        let builder = AgentBuilder::new().apply_settings(settings);
321
322        assert_eq!(builder.config.model.primary, "settings-model");
323        assert_eq!(builder.config.model.small, "settings-small");
324        assert_eq!(builder.config.model.max_tokens, 1000);
325    }
326
327    #[test]
328    fn test_explicit_config_after_settings_overrides() {
329        let settings = Settings {
330            model: Some("settings-model".to_string()),
331            small_model: Some("settings-small".to_string()),
332            max_tokens: Some(1000),
333            ..Default::default()
334        };
335
336        let builder = AgentBuilder::new()
337            .apply_settings(settings)
338            .model("explicit-model")
339            .small_model("explicit-small")
340            .max_tokens(2000);
341
342        assert_eq!(builder.config.model.primary, "explicit-model");
343        assert_eq!(builder.config.model.small, "explicit-small");
344        assert_eq!(builder.config.model.max_tokens, 2000);
345    }
346
347    #[test]
348    fn test_settings_cascade_order() {
349        // Test that settings are applied in order: enterprise → user → project
350        // Later settings override earlier ones
351        let enterprise = Settings {
352            model: Some("enterprise-model".to_string()),
353            small_model: Some("enterprise-small".to_string()),
354            ..Default::default()
355        };
356        let user = Settings {
357            model: Some("user-model".to_string()),
358            ..Default::default()
359        };
360        let project = Settings {
361            small_model: Some("project-small".to_string()),
362            ..Default::default()
363        };
364
365        let builder = AgentBuilder::new()
366            .apply_settings(enterprise)
367            .apply_settings(user)
368            .apply_settings(project);
369
370        // user-model overrides enterprise-model
371        assert_eq!(builder.config.model.primary, "user-model");
372        // project-small overrides enterprise-small
373        assert_eq!(builder.config.model.small, "project-small");
374    }
375
376    #[test]
377    fn test_resource_flags_are_independent() {
378        // Verify that flag methods don't actually load anything, just set flags
379        let builder = AgentBuilder::new()
380            .with_enterprise_resources()
381            .with_user_resources()
382            .with_project_resources()
383            .with_local_resources();
384
385        assert!(builder.load_enterprise);
386        assert!(builder.load_user);
387        assert!(builder.load_project);
388        assert!(builder.load_local);
389
390        // No memory content should be loaded yet (loading happens in build())
391        assert!(builder.memory_provider.is_none());
392    }
393
394    #[test]
395    fn test_chaining_order_does_not_affect_flags() {
396        // Different chaining orders should produce same flag state
397        let builder1 = AgentBuilder::new()
398            .with_enterprise_resources()
399            .with_user_resources()
400            .with_project_resources();
401
402        let builder2 = AgentBuilder::new()
403            .with_project_resources()
404            .with_user_resources()
405            .with_enterprise_resources();
406
407        assert_eq!(builder1.load_enterprise, builder2.load_enterprise);
408        assert_eq!(builder1.load_user, builder2.load_user);
409        assert_eq!(builder1.load_project, builder2.load_project);
410    }
411}