1#![cfg_attr(docsrs, feature(doc_cfg))]
63
64pub use nucel_agent_core::{
66 AgentCapabilities, AgentCost, AgentError, AgentExecutor, AgentResponse, AgentSession,
67 AvailabilityStatus, CachePoint, EventStream, ExecutorType, HookConfig, HookHandler,
68 MessageEvent, PermissionMode, Result, SessionImpl, SessionMetadata, SpawnConfig,
69};
70
71pub use nucel_agent_claude_code::ClaudeCodeExecutor;
73pub use nucel_agent_codex::CodexExecutor;
74pub use nucel_agent_opencode::OpencodeExecutor;
75
76pub fn build_executor(
84 provider: &str,
85 api_key_or_url: Option<String>,
86) -> Option<Box<dyn AgentExecutor>> {
87 match provider {
88 "claude-code" | "claude_code" | "claudecode" => Some(Box::new(ClaudeCodeExecutor::new())),
89 "codex" => Some(Box::new(CodexExecutor::new())),
90 "opencode" => {
91 let mut exec = OpencodeExecutor::new();
92 if let Some(url) = api_key_or_url {
93 exec = OpencodeExecutor::with_base_url(url);
94 }
95 Some(Box::new(exec))
96 }
97 _ => None,
98 }
99}
100
101pub fn available_providers() -> &'static [&'static str] {
103 &["claude-code", "codex", "opencode"]
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn build_claude_code_executor() {
112 let exec = build_executor("claude-code", None).unwrap();
113 assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
114 }
115
116 #[test]
117 fn build_codex_executor() {
118 let exec = build_executor("codex", None).unwrap();
119 assert_eq!(exec.executor_type(), ExecutorType::Codex);
120 }
121
122 #[test]
123 fn build_opencode_executor() {
124 let exec = build_executor("opencode", None).unwrap();
125 assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
126 }
127
128 #[test]
129 fn build_opencode_with_url() {
130 let exec = build_executor("opencode", Some("http://my-server:8080".into())).unwrap();
131 assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
132 }
133
134 #[test]
135 fn unknown_provider_returns_none() {
136 assert!(build_executor("gpt-4", None).is_none());
137 }
138
139 #[test]
140 fn claude_code_aliases_work() {
141 assert!(build_executor("claude_code", None).is_some());
142 assert!(build_executor("claudecode", None).is_some());
143 }
144
145 #[test]
146 fn available_providers_list() {
147 let providers = available_providers();
148 assert_eq!(providers.len(), 3);
149 assert!(providers.contains(&"claude-code"));
150 assert!(providers.contains(&"codex"));
151 assert!(providers.contains(&"opencode"));
152 }
153
154 #[test]
155 fn build_executor_empty_string_returns_none() {
156 assert!(build_executor("", None).is_none());
157 }
158
159 #[test]
160 fn build_executor_case_sensitive() {
161 assert!(build_executor("Claude-Code", None).is_none());
162 assert!(build_executor("CODEX", None).is_none());
163 assert!(build_executor("OpenCode", None).is_none());
164 }
165
166 #[test]
167 fn all_executors_have_capabilities() {
168 for provider in available_providers() {
169 let exec = build_executor(provider, None).unwrap();
170 let caps = exec.capabilities();
171 assert!(caps.token_usage, "{provider} should support token_usage");
173 assert!(caps.autonomous_mode, "{provider} should support autonomous_mode");
175 }
176 }
177
178 #[test]
179 fn all_executors_report_availability() {
180 for provider in available_providers() {
181 let exec = build_executor(provider, None).unwrap();
182 let status = exec.availability();
183 if !status.available {
185 assert!(status.reason.is_some(), "{provider} unavailable but no reason");
186 }
187 }
188 }
189
190 #[test]
191 fn claude_code_api_key_ignored_by_build_executor() {
192 let exec = build_executor("claude-code", Some("sk-test".into())).unwrap();
194 assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
195 }
196
197 #[test]
198 fn codex_api_key_ignored_by_build_executor() {
199 let exec = build_executor("codex", Some("sk-test".into())).unwrap();
200 assert_eq!(exec.executor_type(), ExecutorType::Codex);
201 }
202}