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}