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}