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