Skip to main content

call_coding_clis/invoke/
request.rs

1#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2pub enum RunnerKind {
3    OpenCode,
4    Claude,
5    Codex,
6    Kimi,
7    Cursor,
8    Gemini,
9    RooCode,
10    Crush,
11}
12
13impl RunnerKind {
14    pub(crate) fn from_cli_token(token: &str) -> Option<Self> {
15        match token {
16            "oc" | "opencode" => Some(RunnerKind::OpenCode),
17            "cc" | "claude" => Some(RunnerKind::Claude),
18            "c" | "cx" | "codex" => Some(RunnerKind::Codex),
19            "k" | "kimi" => Some(RunnerKind::Kimi),
20            "cu" | "cursor" => Some(RunnerKind::Cursor),
21            "g" | "gemini" => Some(RunnerKind::Gemini),
22            "rc" | "roocode" => Some(RunnerKind::RooCode),
23            "cr" | "crush" => Some(RunnerKind::Crush),
24            _ => None,
25        }
26    }
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum OutputMode {
31    Text,
32    StreamText,
33    Json,
34    StreamJson,
35    Formatted,
36    StreamFormatted,
37    PassText,
38    StreamPassText,
39    PassJson,
40    StreamPassJson,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Request {
45    prompt: String,
46    prompt_supplied: bool,
47    runner: Option<RunnerKind>,
48    agent: Option<String>,
49    thinking: Option<i32>,
50    show_thinking: Option<bool>,
51    sanitize_osc: Option<bool>,
52    permission_mode: Option<String>,
53    save_session: bool,
54    cleanup_session: bool,
55    provider: Option<String>,
56    model: Option<String>,
57    output_mode: Option<OutputMode>,
58    timeout_secs: Option<u64>,
59}
60
61impl Request {
62    pub fn new(prompt: impl Into<String>) -> Self {
63        Self {
64            prompt: prompt.into(),
65            prompt_supplied: true,
66            runner: None,
67            agent: None,
68            thinking: None,
69            show_thinking: None,
70            sanitize_osc: None,
71            permission_mode: None,
72            save_session: false,
73            cleanup_session: false,
74            provider: None,
75            model: None,
76            output_mode: None,
77            timeout_secs: None,
78        }
79    }
80
81    pub fn with_runner(mut self, runner: RunnerKind) -> Self {
82        self.runner = Some(runner);
83        self
84    }
85
86    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
87        self.provider = Some(provider.into());
88        self
89    }
90
91    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
92        self.agent = Some(agent.into());
93        self
94    }
95
96    pub fn with_thinking(mut self, thinking: i32) -> Self {
97        self.thinking = Some(thinking);
98        self
99    }
100
101    pub fn with_show_thinking(mut self, enabled: bool) -> Self {
102        self.show_thinking = Some(enabled);
103        self
104    }
105
106    pub fn with_sanitize_osc(mut self, enabled: bool) -> Self {
107        self.sanitize_osc = Some(enabled);
108        self
109    }
110
111    pub fn with_permission_mode(mut self, mode: impl Into<String>) -> Self {
112        self.permission_mode = Some(mode.into());
113        self
114    }
115
116    pub fn with_save_session(mut self, enabled: bool) -> Self {
117        self.save_session = enabled;
118        self
119    }
120
121    pub fn with_cleanup_session(mut self, enabled: bool) -> Self {
122        self.cleanup_session = enabled;
123        self
124    }
125
126    pub fn with_model(mut self, model: impl Into<String>) -> Self {
127        self.model = Some(model.into());
128        self
129    }
130
131    pub fn with_output_mode(mut self, output_mode: OutputMode) -> Self {
132        self.output_mode = Some(output_mode);
133        self
134    }
135
136    pub fn with_timeout_secs(mut self, secs: u64) -> Self {
137        self.timeout_secs = Some(secs);
138        self
139    }
140
141    pub fn timeout_secs(&self) -> Option<u64> {
142        self.timeout_secs
143    }
144
145    pub fn prompt(&self) -> &str {
146        &self.prompt
147    }
148
149    pub fn runner(&self) -> Option<RunnerKind> {
150        self.runner
151    }
152
153    pub fn provider(&self) -> Option<&str> {
154        self.provider.as_deref()
155    }
156
157    pub fn model(&self) -> Option<&str> {
158        self.model.as_deref()
159    }
160
161    pub fn output_mode(&self) -> Option<OutputMode> {
162        self.output_mode
163    }
164
165    pub(crate) fn prompt_text(&self) -> &str {
166        &self.prompt
167    }
168
169    pub(crate) fn runner_kind(&self) -> Option<RunnerKind> {
170        self.runner
171    }
172
173    pub(crate) fn provider_text(&self) -> Option<&str> {
174        self.provider.as_deref()
175    }
176
177    pub(crate) fn model_text(&self) -> Option<&str> {
178        self.model.as_deref()
179    }
180
181    pub(crate) fn output_mode_kind(&self) -> Option<OutputMode> {
182        self.output_mode
183    }
184
185    pub(crate) fn from_parsed_args(parsed: &crate::parser::ParsedArgs) -> Result<Self, String> {
186        let runner = parsed
187            .runner
188            .as_deref()
189            .and_then(RunnerKind::from_cli_token);
190        if parsed.runner.is_some() && runner.is_none() {
191            return Err("unknown runner selector".to_string());
192        }
193
194        let output_mode = match parsed.output_mode.as_deref() {
195            Some("") => {
196                return Err(
197                    "output mode requires one of: text, stream-text, json, stream-json, formatted, stream-formatted, pass-text, pt, stream-pass-text, stream-pt, pass-json, pj, stream-pass-json, stream-pj"
198                        .to_string(),
199                )
200            }
201            Some(value) => Some(OutputMode::from_cli_value(value).ok_or_else(|| {
202                "output mode must be one of: text, stream-text, json, stream-json, formatted, stream-formatted, pass-text, pt, stream-pass-text, stream-pt, pass-json, pj, stream-pass-json, stream-pj"
203                    .to_string()
204            })?),
205            None => None,
206        };
207
208        Ok(Self {
209            prompt: parsed.prompt.clone(),
210            prompt_supplied: parsed.prompt_supplied,
211            runner,
212            agent: parsed.alias.clone(),
213            thinking: parsed.thinking,
214            show_thinking: parsed.show_thinking,
215            sanitize_osc: parsed.sanitize_osc,
216            permission_mode: parsed.permission_mode.clone(),
217            save_session: parsed.save_session,
218            cleanup_session: parsed.cleanup_session,
219            provider: parsed.provider.clone(),
220            model: parsed.model.clone(),
221            output_mode,
222            timeout_secs: parsed.timeout_secs,
223        })
224    }
225
226    pub(crate) fn to_cli_tokens(&self) -> Vec<String> {
227        let mut tokens = Vec::new();
228        if let Some(runner) = self.runner_kind() {
229            tokens.push(runner.as_cli_token().to_string());
230        }
231        if let Some(thinking) = self.thinking {
232            tokens.push(format!("+{thinking}"));
233        }
234        if let Some(show_thinking) = self.show_thinking {
235            tokens.push(if show_thinking {
236                "--show-thinking".to_string()
237            } else {
238                "--no-show-thinking".to_string()
239            });
240        }
241        if let Some(sanitize_osc) = self.sanitize_osc {
242            tokens.push(if sanitize_osc {
243                "--sanitize-osc".to_string()
244            } else {
245                "--no-sanitize-osc".to_string()
246            });
247        }
248        if let Some(permission_mode) = self.permission_mode.as_deref() {
249            tokens.push("--permission-mode".to_string());
250            tokens.push(permission_mode.to_string());
251        }
252        if self.save_session {
253            tokens.push("--save-session".to_string());
254        }
255        if self.cleanup_session {
256            tokens.push("--cleanup-session".to_string());
257        }
258        if let Some(agent) = self.agent.as_deref() {
259            tokens.push(format!("@{agent}"));
260        }
261        if let Some(provider) = self.provider_text() {
262            if let Some(model) = self.model_text() {
263                tokens.push(format!(":{provider}:{model}"));
264            }
265        } else if let Some(model) = self.model_text() {
266            tokens.push(format!(":{model}"));
267        }
268        if let Some(output_mode) = self.output_mode_kind() {
269            tokens.push("--output-mode".to_string());
270            tokens.push(output_mode.as_cli_value().to_string());
271        }
272        if let Some(timeout) = self.timeout_secs {
273            tokens.push("--timeout-secs".to_string());
274            tokens.push(timeout.to_string());
275        }
276        if self.prompt_supplied {
277            tokens.push(self.prompt_text().to_string());
278        }
279        tokens
280    }
281}
282
283impl RunnerKind {
284    pub(crate) fn as_cli_token(self) -> &'static str {
285        match self {
286            RunnerKind::OpenCode => "oc",
287            RunnerKind::Claude => "cc",
288            RunnerKind::Codex => "c",
289            RunnerKind::Kimi => "k",
290            RunnerKind::Cursor => "cu",
291            RunnerKind::Gemini => "g",
292            RunnerKind::RooCode => "rc",
293            RunnerKind::Crush => "cr",
294        }
295    }
296}
297
298impl OutputMode {
299    pub(crate) fn as_cli_value(self) -> &'static str {
300        match self {
301            OutputMode::Text => "text",
302            OutputMode::StreamText => "stream-text",
303            OutputMode::Json => "json",
304            OutputMode::StreamJson => "stream-json",
305            OutputMode::Formatted => "formatted",
306            OutputMode::StreamFormatted => "stream-formatted",
307            OutputMode::PassText => "pass-text",
308            OutputMode::StreamPassText => "stream-pass-text",
309            OutputMode::PassJson => "pass-json",
310            OutputMode::StreamPassJson => "stream-pass-json",
311        }
312    }
313
314    pub(crate) fn from_cli_value(value: &str) -> Option<Self> {
315        match value {
316            "text" | "pt" => Some(OutputMode::Text),
317            "stream-text" | "stream-pt" => Some(OutputMode::StreamText),
318            "json" | "pj" => Some(OutputMode::Json),
319            "stream-json" | "stream-pj" => Some(OutputMode::StreamJson),
320            "formatted" => Some(OutputMode::Formatted),
321            "stream-formatted" => Some(OutputMode::StreamFormatted),
322            "pass-text" => Some(OutputMode::PassText),
323            "stream-pass-text" => Some(OutputMode::StreamPassText),
324            "pass-json" => Some(OutputMode::PassJson),
325            "stream-pass-json" => Some(OutputMode::StreamPassJson),
326            _ => None,
327        }
328    }
329}