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