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