1use crate::error::BobError;
10use crate::keychain::resolve_api_key;
11use cli_stream::{spawn_streaming, ProcessEvent, ProcessHandle};
12use serde::{Deserialize, Serialize};
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum BobChatMode {
22 Plan,
23 Code,
24 Advanced,
25 Ask,
26}
27
28impl BobChatMode {
29 pub fn as_cli_value(self) -> &'static str {
30 match self {
31 Self::Plan => "plan",
32 Self::Code => "code",
33 Self::Advanced => "advanced",
34 Self::Ask => "ask",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum BobApprovalMode {
46 Default,
47 AutoEdit,
48 Yolo,
49}
50
51impl BobApprovalMode {
52 pub fn as_cli_value(self) -> &'static str {
53 match self {
54 Self::Default => "default",
55 Self::AutoEdit => "auto_edit",
56 Self::Yolo => "yolo",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "camelCase")]
65pub struct RunBobOptions {
66 pub prompt: String,
67 #[serde(default = "default_chat_mode")]
68 pub chat_mode: BobChatMode,
69 #[serde(default = "default_approval_mode")]
70 pub approval_mode: BobApprovalMode,
71 #[serde(default = "default_max_coins")]
72 pub max_coins: u32,
73 pub cwd: Option<PathBuf>,
77 #[serde(default)]
81 pub bob_executable: Option<PathBuf>,
82 #[serde(default)]
86 pub extra_args: Vec<String>,
87 #[serde(default)]
91 pub resume: Option<String>,
92}
93
94fn default_chat_mode() -> BobChatMode { BobChatMode::Ask }
95fn default_approval_mode() -> BobApprovalMode { BobApprovalMode::Default }
96fn default_max_coins() -> u32 { 30 }
97
98pub fn spawn_bob<F>(
107 opts: RunBobOptions,
108 run_id: String,
109 callback: F,
110) -> Result<ProcessHandle, BobError>
111where
112 F: FnMut(ProcessEvent) + Send + Sync + Clone + 'static,
113{
114 let args = build_args(&opts);
115 let api_key = resolve_api_key().map(|(value, _)| value).unwrap_or_default();
116 let program: PathBuf = opts.bob_executable.clone().unwrap_or_else(|| PathBuf::from("bob"));
117 let cwd = opts.cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
118 spawn_bob_raw(program, args, api_key, cwd, run_id, callback)
119}
120
121pub fn spawn_bob_raw<F>(
128 program: PathBuf,
129 args: Vec<String>,
130 api_key: String,
131 cwd: PathBuf,
132 run_id: String,
133 callback: F,
134) -> Result<ProcessHandle, BobError>
135where
136 F: FnMut(ProcessEvent) + Send + Sync + Clone + 'static,
137{
138 let handle = spawn_streaming(
139 program,
140 args,
141 vec![("BOBSHELL_API_KEY".to_owned(), api_key)],
142 cwd,
143 run_id,
144 callback,
145 )?; Ok(handle)
147}
148
149fn build_args(opts: &RunBobOptions) -> Vec<String> {
152 let mut args = vec![
153 opts.prompt.clone(),
154 "--chat-mode".to_owned(),
155 opts.chat_mode.as_cli_value().to_owned(),
156 "--output-format".to_owned(),
157 "stream-json".to_owned(),
158 "--approval-mode".to_owned(),
159 opts.approval_mode.as_cli_value().to_owned(),
160 "--accept-license".to_owned(),
161 "--max-coins".to_owned(),
162 opts.max_coins.to_string(),
163 ];
164 if let Some(session_id) = &opts.resume {
167 args.push("--resume".to_owned());
168 args.push(session_id.clone());
169 }
170 args.extend(opts.extra_args.iter().cloned());
172 args
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn opts(extra_args: Vec<String>) -> RunBobOptions {
180 RunBobOptions {
181 prompt: "hi".to_owned(),
182 chat_mode: BobChatMode::Ask,
183 approval_mode: BobApprovalMode::Default,
184 max_coins: 30,
185 cwd: None,
186 bob_executable: None,
187 extra_args,
188 resume: None,
189 }
190 }
191
192 #[test]
193 fn build_args_appends_extra_args_after_bobs_own() {
194 let args = build_args(&opts(vec!["--foo".to_owned(), "bar".to_owned()]));
195 assert_eq!(args.first().map(String::as_str), Some("hi"));
197 assert!(args.contains(&"stream-json".to_owned()));
198 assert!(args.ends_with(&["--foo".to_owned(), "bar".to_owned()]));
200 }
201
202 #[test]
203 fn build_args_with_no_extra_is_unchanged() {
204 let args = build_args(&opts(Vec::new()));
205 assert_eq!(args.last().map(String::as_str), Some("30"));
206 }
207
208 #[test]
209 fn build_args_resume_adds_session_flag() {
210 let mut o = opts(Vec::new());
211 o.resume = Some("sess-7".to_owned());
212 let args = build_args(&o);
213 let i = args.iter().position(|a| a == "--resume").expect("--resume");
214 assert_eq!(args.get(i + 1).map(String::as_str), Some("sess-7"));
215 assert_eq!(args.first().map(String::as_str), Some("hi"));
217 }
218}