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 "gpt-5.2-codex".into(),
208 "gpt-5.2".into(),
209 ]),
210 _ => None, }
212}
213
214pub fn handle_capabilities(runner_str: &str, format: RunnerFormat) -> anyhow::Result<()> {
216 let runner: Runner = runner_str
217 .parse()
218 .map_err(|_| anyhow::anyhow!("unknown runner: {}", runner_str))?;
219
220 let bin_name = get_bin_name(&runner);
222
223 let report = get_runner_capabilities(&runner, &bin_name);
224
225 match format {
226 RunnerFormat::Text => print_capabilities_text(&report),
227 RunnerFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
228 }
229
230 Ok(())
231}
232
233fn get_bin_name(runner: &Runner) -> String {
234 match runner {
235 Runner::Codex => "codex".into(),
236 Runner::Opencode => "opencode".into(),
237 Runner::Gemini => "gemini".into(),
238 Runner::Claude => "claude".into(),
239 Runner::Cursor => "agent".into(), Runner::Kimi => "kimi".into(),
241 Runner::Pi => "pi".into(),
242 Runner::Plugin(id) => id.clone(),
243 }
244}
245
246fn print_capabilities_text(report: &RunnerCapabilityReport) {
247 println!("Runner: {} ({})", report.name, report.runner);
248 println!();
249
250 println!("Binary:");
252 if report.binary.installed {
253 println!(" Status: installed");
254 if let Some(ref v) = report.binary.version {
255 println!(" Version: {}", v);
256 }
257 } else {
258 println!(" Status: NOT INSTALLED");
259 if let Some(ref e) = report.binary.error {
260 println!(" Error: {}", e);
261 }
262 }
263 println!();
264
265 println!("Models:");
267 println!(" Default: {}", report.default_model);
268 if let Some(ref models) = report.allowed_models {
269 println!(" Allowed: {}", models.join(", "));
270 } else {
271 println!(" Allowed: (any model ID)");
272 }
273 println!();
274
275 println!("Features:");
277 println!(
278 " Session resume: {}",
279 if report.supports_session_resume {
280 "yes"
281 } else {
282 "no"
283 }
284 );
285 if report.requires_managed_session_id {
286 println!(" Managed session ID: required (Ralph supplies session ID)");
287 }
288 println!(
289 " Reasoning effort: {}",
290 if report.features.reasoning_effort {
291 "yes"
292 } else {
293 "no"
294 }
295 );
296 println!(
297 " Plan mode: {}",
298 if report.features.plan_mode {
299 "yes"
300 } else {
301 "no"
302 }
303 );
304 println!(
305 " Verbose output: {}",
306 if report.features.verbose { "yes" } else { "no" }
307 );
308
309 if report.features.sandbox.supported {
311 println!(
312 " Sandbox: {} (supported)",
313 report.features.sandbox.modes.join(", ")
314 );
315 } else {
316 println!(" Sandbox: not supported");
317 }
318
319 if !report.features.approval_modes.is_empty() {
321 println!(
322 " Approval modes: {}",
323 report.features.approval_modes.join(", ")
324 );
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn codex_has_reasoning_effort_support() {
334 let features = get_runner_features(&Runner::Codex);
335 assert!(features.reasoning_effort);
336 assert!(!features.plan_mode);
337 }
338
339 #[test]
340 fn cursor_has_plan_mode_support() {
341 let features = get_runner_features(&Runner::Cursor);
342 assert!(features.plan_mode);
343 assert!(!features.reasoning_effort);
344 }
345
346 #[test]
347 fn codex_has_restricted_models() {
348 let report = get_runner_capabilities(&Runner::Codex, "codex");
349 assert!(report.allowed_models.is_some());
350 let models = report.allowed_models.unwrap();
351 assert!(models.contains(&"gpt-5.4".to_string()));
352 assert!(models.contains(&"gpt-5.3-codex".to_string()));
353 assert!(!models.contains(&"sonnet".to_string()));
354 }
355
356 #[test]
357 fn claude_allows_arbitrary_models() {
358 let report = get_runner_capabilities(&Runner::Claude, "claude");
359 assert!(report.allowed_models.is_none());
360 }
361
362 #[test]
363 fn kimi_requires_managed_session_id() {
364 let report = get_runner_capabilities(&Runner::Kimi, "kimi");
365 assert!(report.requires_managed_session_id);
366 }
367}