1use serde::Serialize;
12
13use crate::cli::runner::RunnerFormat;
14use crate::contracts::Runner;
15use crate::runner::default_model_for_runner;
16use crate::runner::{BuiltInRunnerPlugin, RunnerPlugin};
17
18use super::detection::check_runner_binary;
19
20#[derive(Debug, Clone, Serialize)]
22pub struct RunnerCapabilityReport {
23 pub runner: String,
25 pub name: String,
27 pub supports_session_resume: bool,
29 pub requires_managed_session_id: bool,
31 pub features: RunnerFeatures,
33 pub allowed_models: Option<Vec<String>>,
35 pub default_model: String,
37 pub binary: BinaryInfo,
39}
40
41#[derive(Debug, Clone, Serialize)]
42pub struct RunnerFeatures {
43 pub reasoning_effort: bool,
45 pub sandbox: SandboxSupport,
47 pub plan_mode: bool,
49 pub verbose: bool,
51 pub approval_modes: Vec<String>,
53}
54
55#[derive(Debug, Clone, Serialize)]
56pub struct SandboxSupport {
57 pub supported: bool,
58 pub modes: Vec<String>,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub struct BinaryInfo {
63 pub installed: bool,
64 pub version: Option<String>,
65 pub error: Option<String>,
66}
67
68pub fn get_runner_capabilities(runner: &Runner, bin_name: &str) -> RunnerCapabilityReport {
70 let plugin = runner_to_plugin(runner);
71 let metadata = plugin.metadata();
72
73 let binary_status = check_runner_binary(bin_name);
75 let binary_info = BinaryInfo {
76 installed: binary_status.installed,
77 version: binary_status.version,
78 error: binary_status.error,
79 };
80
81 let features = get_runner_features(runner);
83
84 let allowed_models = get_allowed_models(runner);
86
87 let default_model = default_model_for_runner(runner);
89
90 RunnerCapabilityReport {
91 runner: runner.id().to_string(),
92 name: metadata.name,
93 supports_session_resume: metadata.supports_resume,
94 requires_managed_session_id: plugin.requires_managed_session_id(),
95 features,
96 allowed_models,
97 default_model: default_model.as_str().to_string(),
98 binary: binary_info,
99 }
100}
101
102fn runner_to_plugin(runner: &Runner) -> BuiltInRunnerPlugin {
103 match runner {
104 Runner::Codex => BuiltInRunnerPlugin::Codex,
105 Runner::Opencode => BuiltInRunnerPlugin::Opencode,
106 Runner::Gemini => BuiltInRunnerPlugin::Gemini,
107 Runner::Claude => BuiltInRunnerPlugin::Claude,
108 Runner::Kimi => BuiltInRunnerPlugin::Kimi,
109 Runner::Pi => BuiltInRunnerPlugin::Pi,
110 Runner::Cursor => BuiltInRunnerPlugin::Cursor,
111 Runner::Plugin(_) => BuiltInRunnerPlugin::Claude, }
113}
114
115pub(crate) fn get_runner_features(runner: &Runner) -> RunnerFeatures {
116 match runner {
117 Runner::Codex => RunnerFeatures {
118 reasoning_effort: true,
119 sandbox: SandboxSupport {
120 supported: true,
121 modes: vec!["default".into(), "enabled".into(), "disabled".into()],
122 },
123 plan_mode: false,
124 verbose: false,
125 approval_modes: vec!["config_file".into()], },
127 Runner::Claude => RunnerFeatures {
128 reasoning_effort: false,
129 sandbox: SandboxSupport {
130 supported: false,
131 modes: vec![],
132 },
133 plan_mode: false,
134 verbose: true,
135 approval_modes: vec!["accept_edits".into(), "bypass_permissions".into()],
136 },
137 Runner::Gemini => RunnerFeatures {
138 reasoning_effort: false,
139 sandbox: SandboxSupport {
140 supported: true,
141 modes: vec!["default".into(), "enabled".into()],
142 },
143 plan_mode: false,
144 verbose: false,
145 approval_modes: vec!["yolo".into(), "auto_edit".into()],
146 },
147 Runner::Cursor => RunnerFeatures {
148 reasoning_effort: false,
149 sandbox: SandboxSupport {
150 supported: true,
151 modes: vec!["enabled".into(), "disabled".into()],
152 },
153 plan_mode: true,
154 verbose: false,
155 approval_modes: vec!["force".into()],
156 },
157 Runner::Opencode => RunnerFeatures {
158 reasoning_effort: false,
159 sandbox: SandboxSupport {
160 supported: false,
161 modes: vec![],
162 },
163 plan_mode: false,
164 verbose: false,
165 approval_modes: vec![],
166 },
167 Runner::Kimi => RunnerFeatures {
168 reasoning_effort: false,
169 sandbox: SandboxSupport {
170 supported: false,
171 modes: vec![],
172 },
173 plan_mode: false,
174 verbose: false,
175 approval_modes: vec!["yolo".into()],
176 },
177 Runner::Pi => RunnerFeatures {
178 reasoning_effort: false,
179 sandbox: SandboxSupport {
180 supported: true,
181 modes: vec!["default".into(), "enabled".into()],
182 },
183 plan_mode: false,
184 verbose: false,
185 approval_modes: vec!["print".into()],
186 },
187 Runner::Plugin(_) => RunnerFeatures {
188 reasoning_effort: false,
189 sandbox: SandboxSupport {
190 supported: false,
191 modes: vec![],
192 },
193 plan_mode: false,
194 verbose: false,
195 approval_modes: vec![],
196 },
197 }
198}
199
200fn get_allowed_models(runner: &Runner) -> Option<Vec<String>> {
201 match runner {
202 Runner::Codex => Some(vec![
203 "gpt-5.4".into(),
204 "gpt-5.3-codex".into(),
205 "gpt-5.3-codex-spark".into(),
206 "gpt-5.3".into(),
207 ]),
208 _ => None, }
210}
211
212pub fn handle_capabilities(runner_str: &str, format: RunnerFormat) -> anyhow::Result<()> {
214 let runner: Runner = runner_str
215 .parse()
216 .map_err(|_| anyhow::anyhow!("unknown runner: {}", runner_str))?;
217
218 let bin_name = get_bin_name(&runner);
220
221 let report = get_runner_capabilities(&runner, &bin_name);
222
223 match format {
224 RunnerFormat::Text => print_capabilities_text(&report),
225 RunnerFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
226 }
227
228 Ok(())
229}
230
231fn get_bin_name(runner: &Runner) -> String {
232 match runner {
233 Runner::Codex => "codex".into(),
234 Runner::Opencode => "opencode".into(),
235 Runner::Gemini => "gemini".into(),
236 Runner::Claude => "claude".into(),
237 Runner::Cursor => "agent".into(), Runner::Kimi => "kimi".into(),
239 Runner::Pi => "pi".into(),
240 Runner::Plugin(id) => id.clone(),
241 }
242}
243
244fn print_capabilities_text(report: &RunnerCapabilityReport) {
245 println!("Runner: {} ({})", report.name, report.runner);
246 println!();
247
248 println!("Binary:");
250 if report.binary.installed {
251 println!(" Status: installed");
252 if let Some(ref v) = report.binary.version {
253 println!(" Version: {}", v);
254 }
255 } else {
256 println!(" Status: NOT INSTALLED");
257 if let Some(ref e) = report.binary.error {
258 println!(" Error: {}", e);
259 }
260 }
261 println!();
262
263 println!("Models:");
265 println!(" Default: {}", report.default_model);
266 if let Some(ref models) = report.allowed_models {
267 println!(" Allowed: {}", models.join(", "));
268 } else {
269 println!(" Allowed: (any model ID)");
270 }
271 println!();
272
273 println!("Features:");
275 println!(
276 " Session resume: {}",
277 if report.supports_session_resume {
278 "yes"
279 } else {
280 "no"
281 }
282 );
283 if report.requires_managed_session_id {
284 println!(" Managed session ID: required (Ralph supplies session ID)");
285 }
286 println!(
287 " Reasoning effort: {}",
288 if report.features.reasoning_effort {
289 "yes"
290 } else {
291 "no"
292 }
293 );
294 println!(
295 " Plan mode: {}",
296 if report.features.plan_mode {
297 "yes"
298 } else {
299 "no"
300 }
301 );
302 println!(
303 " Verbose output: {}",
304 if report.features.verbose { "yes" } else { "no" }
305 );
306
307 if report.features.sandbox.supported {
309 println!(
310 " Sandbox: {} (supported)",
311 report.features.sandbox.modes.join(", ")
312 );
313 } else {
314 println!(" Sandbox: not supported");
315 }
316
317 if !report.features.approval_modes.is_empty() {
319 println!(
320 " Approval modes: {}",
321 report.features.approval_modes.join(", ")
322 );
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn codex_has_reasoning_effort_support() {
332 let features = get_runner_features(&Runner::Codex);
333 assert!(features.reasoning_effort);
334 assert!(!features.plan_mode);
335 }
336
337 #[test]
338 fn cursor_has_plan_mode_support() {
339 let features = get_runner_features(&Runner::Cursor);
340 assert!(features.plan_mode);
341 assert!(!features.reasoning_effort);
342 }
343
344 #[test]
345 fn codex_has_restricted_models() {
346 let report = get_runner_capabilities(&Runner::Codex, "codex");
347 assert!(report.allowed_models.is_some());
348 let models = report.allowed_models.unwrap();
349 assert!(models.contains(&"gpt-5.4".to_string()));
350 assert!(models.contains(&"gpt-5.3-codex".to_string()));
351 assert!(!models.contains(&"sonnet".to_string()));
352 }
353
354 #[test]
355 fn claude_allows_arbitrary_models() {
356 let report = get_runner_capabilities(&Runner::Claude, "claude");
357 assert!(report.allowed_models.is_none());
358 }
359
360 #[test]
361 fn kimi_requires_managed_session_id() {
362 let report = get_runner_capabilities(&Runner::Kimi, "kimi");
363 assert!(report.requires_managed_session_id);
364 }
365}