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}