1use crate::api::{Anthropic, Api};
6use crate::provider::codex::api::{Network as OpenAiNetwork, OpenAi};
7use crate::store::Host;
8use crate::time::{Clock, SystemClock};
9use std::path::PathBuf;
10use std::sync::Arc;
11
12#[derive(Clone, Debug)]
13pub struct Context {
14 pub(crate) home: PathBuf,
15 pub(crate) pitboard_home: PathBuf,
16 pub(crate) claude_config_dir: Option<String>,
18 pub(crate) secure_storage_dir: Option<String>,
21 pub(crate) user: Option<String>,
23 pub(crate) custom_oauth: bool,
26 pub(crate) overriding_auth: Vec<String>,
30 pub(crate) argv_fallback: bool,
35 pub(crate) caller: String,
38 pub(crate) claude_program: PathBuf,
40 pub(crate) api_base: Option<String>,
42 pub(crate) hover_rest: bool,
44 pub(crate) codex_home: Option<String>,
46 pub(crate) codex_program: PathBuf,
48 pub(crate) schedule_program: Option<PathBuf>,
52 pub(crate) search_path: Option<std::ffi::OsString>,
57 #[cfg(target_os = "macos")]
60 pub(crate) launchd_job: Option<String>,
61 pub(crate) clock: Arc<dyn Clock>,
64 pub(crate) host: Arc<dyn Host>,
66 pub(crate) api: Arc<dyn Api>,
68 pub(crate) openai: Arc<dyn OpenAi>,
70}
71
72impl Context {
73 pub(crate) fn now(&self) -> i64 {
75 self.clock.now()
76 }
77
78 pub(crate) fn now_millis(&self) -> i64 {
80 self.clock.now_millis()
81 }
82
83 pub(crate) fn home(&self) -> &std::path::Path {
85 &self.home
86 }
87
88 pub(crate) fn host(&self) -> &dyn Host {
90 self.host.as_ref()
91 }
92
93 pub(crate) fn openai(&self) -> &dyn OpenAi {
95 self.openai.as_ref()
96 }
97
98 pub(crate) fn api(&self) -> &dyn Api {
99 self.api.as_ref()
100 }
101
102 pub fn new(home: PathBuf) -> Context {
106 Context {
107 pitboard_home: home.join(".pitboard"),
108 home,
109 claude_config_dir: None,
110 secure_storage_dir: None,
111 user: None,
112 custom_oauth: false,
113 argv_fallback: true,
114 overriding_auth: Vec::new(),
115 caller: "unknown".into(),
116 claude_program: PathBuf::from("claude"),
117 api_base: None,
118 hover_rest: false,
119 codex_home: None,
120 codex_program: PathBuf::from("codex"),
121 schedule_program: None,
122 search_path: None,
123 #[cfg(target_os = "macos")]
124 launchd_job: None,
125 clock: Arc::new(SystemClock),
126 host: crate::store::host(),
127 api: Arc::new(Anthropic),
128 openai: Arc::new(OpenAiNetwork),
129 }
130 }
131
132 pub fn with_codex_home(mut self, dir: String) -> Context {
134 self.codex_home = Some(dir).filter(|d| !d.is_empty());
135 self
136 }
137
138 pub(crate) fn codex_home(&self) -> Option<&str> {
139 self.codex_home.as_deref()
140 }
141
142 pub fn with_pitboard_home(mut self, dir: PathBuf) -> Context {
143 self.pitboard_home = dir;
144 self
145 }
146
147 pub fn with_claude_config_dir(mut self, dir: String) -> Context {
149 self.claude_config_dir = Some(dir).filter(|d| !d.is_empty());
150 self
151 }
152
153 pub fn with_secure_storage_dir(mut self, dir: String) -> Context {
156 self.secure_storage_dir = Some(dir);
157 self
158 }
159
160 pub fn with_argv_fallback(mut self, allowed: bool) -> Context {
163 self.argv_fallback = allowed;
164 self
165 }
166
167 pub fn custom_oauth(&self) -> bool {
169 self.custom_oauth
170 }
171
172 pub fn claude_program(&self) -> &std::path::Path {
174 &self.claude_program
175 }
176
177 pub fn argv_fallback(&self) -> bool {
179 self.argv_fallback
180 }
181
182 pub fn overriding_auth(&self) -> &[String] {
184 &self.overriding_auth
185 }
186
187 pub fn with_caller(mut self, caller: String) -> Context {
189 self.caller = caller;
190 self
191 }
192
193 pub fn with_user(mut self, user: String) -> Context {
194 self.user = Some(user);
195 self
196 }
197
198 pub fn with_claude_program(mut self, program: PathBuf) -> Context {
200 self.claude_program = program;
201 self
202 }
203
204 pub fn with_codex_program(mut self, program: PathBuf) -> Context {
206 self.codex_program = program;
207 self
208 }
209
210 pub fn codex_program(&self) -> &std::path::Path {
212 &self.codex_program
213 }
214
215 pub fn with_schedule_program(mut self, program: PathBuf) -> Context {
218 self.schedule_program = Some(program);
219 self
220 }
221
222 pub fn schedule_program(&self) -> Option<&std::path::Path> {
224 self.schedule_program.as_deref()
225 }
226
227 pub fn with_search_path(mut self, path: String) -> Context {
232 self.search_path = Some(path.into());
233 self
234 }
235
236 pub(crate) fn search_path(&self) -> std::ffi::OsString {
238 self.search_path
239 .clone()
240 .or_else(|| std::env::var_os("PATH"))
241 .unwrap_or_default()
242 }
243
244 #[cfg(target_os = "macos")]
247 pub(crate) fn launchd_job(&self) -> Option<String> {
248 self.launchd_job
249 .clone()
250 .or_else(|| std::env::var("XPC_SERVICE_NAME").ok())
251 }
252
253 #[cfg(all(target_os = "macos", test))]
255 pub(crate) fn with_launchd_job(mut self, label: String) -> Context {
256 self.launchd_job = Some(label);
257 self
258 }
259
260 pub fn program_for(&self, tool: crate::provider::ProviderId) -> &std::path::Path {
262 match tool {
263 crate::provider::ProviderId::Claude => &self.claude_program,
264 crate::provider::ProviderId::Codex => &self.codex_program,
265 }
266 }
267
268 pub fn from_env() -> Context {
269 let var = |name: &str| std::env::var(name).ok();
270 let home = std::env::var_os("HOME")
271 .map(PathBuf::from)
272 .unwrap_or_default();
273 Context {
274 pitboard_home: std::env::var_os("PITBOARD_HOME")
275 .map(PathBuf::from)
276 .unwrap_or_else(|| home.join(".pitboard")),
277 home,
278 claude_config_dir: var("CLAUDE_CONFIG_DIR").filter(|v| !v.is_empty()),
279 secure_storage_dir: var("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
280 user: var("USER"),
281 custom_oauth: var("CLAUDE_CODE_CUSTOM_OAUTH_URL").is_some_and(|v| !v.is_empty()),
282 argv_fallback: !var("PITBOARD_NO_ARGV").is_some_and(|v| v == "1"),
283 overriding_auth: crate::settings::OVERRIDING_ENV
284 .iter()
285 .filter(|name| var(name).is_some_and(|v| !v.is_empty()))
286 .map(|name| (*name).to_string())
287 .collect(),
288 caller: "cli".into(),
289 claude_program: PathBuf::from("claude"),
290 api_base: var("PITBOARD_API_BASE"),
291 hover_rest: var("CLAUDE_CODE_HOVER_REST").is_some_and(|v| v == "1" || v == "true"),
292 codex_home: var("CODEX_HOME").filter(|v| !v.is_empty()),
293 codex_program: PathBuf::from("codex"),
294 schedule_program: None,
295 search_path: None,
296 #[cfg(target_os = "macos")]
297 launchd_job: None,
298 clock: Arc::new(SystemClock),
299 host: crate::store::host(),
300 api: Arc::new(Anthropic),
301 openai: Arc::new(OpenAiNetwork),
302 }
303 }
304
305 #[cfg(any(test, feature = "test-support"))]
309 #[doc(hidden)]
310 pub fn with_scripted_api(mut self, api: Arc<crate::api::scripted::ScriptedApi>) -> Context {
311 self.api = api.clone();
312 self.openai = api;
313 self
314 }
315
316 #[cfg(any(test, feature = "test-support"))]
319 #[doc(hidden)]
320 pub fn with_memory_stores(mut self, memory: Arc<crate::store::memory::MemoryHost>) -> Context {
321 self.host = memory;
322 self
323 }
324
325 #[cfg(any(test, feature = "test-support"))]
328 #[doc(hidden)]
329 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Context {
330 self.clock = clock;
331 self
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn an_explicit_context_reads_claude_codes_settings_the_way_the_environment_does() {
341 let ctx = Context::new(PathBuf::from("/home/x"))
342 .with_claude_config_dir(String::new())
343 .with_secure_storage_dir(String::new());
344 assert_eq!(ctx.pitboard_home, PathBuf::from("/home/x/.pitboard"));
345 assert_eq!(ctx.claude_config_dir, None, "empty means unset");
346 assert_eq!(
347 ctx.secure_storage_dir.as_deref(),
348 Some(""),
349 "empty is set, and pins the default slot"
350 );
351 assert_eq!(ctx.claude_program, PathBuf::from("claude"));
352 }
353
354 #[test]
355 fn only_a_front_end_that_names_one_changes_what_the_schedule_runs() {
356 assert_eq!(Context::from_env().schedule_program(), None);
357 let ctx = Context::new(PathBuf::from("/Users/x"));
358 assert_eq!(ctx.schedule_program(), None);
359 let bundled = PathBuf::from("/Applications/Pitboard.app/Contents/Helpers/pitboard");
360 assert_eq!(
361 ctx.with_schedule_program(bundled.clone())
362 .schedule_program(),
363 Some(bundled.as_path())
364 );
365 }
366}