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