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) search_path: Option<std::ffi::OsString>,
53 pub(crate) clock: Arc<dyn Clock>,
56 pub(crate) host: Arc<dyn Host>,
58 pub(crate) api: Arc<dyn Api>,
60 pub(crate) openai: Arc<dyn OpenAi>,
62}
63
64impl Context {
65 pub(crate) fn now(&self) -> i64 {
67 self.clock.now()
68 }
69
70 pub(crate) fn now_millis(&self) -> i64 {
72 self.clock.now_millis()
73 }
74
75 pub(crate) fn home(&self) -> &std::path::Path {
77 &self.home
78 }
79
80 pub(crate) fn host(&self) -> &dyn Host {
82 self.host.as_ref()
83 }
84
85 pub(crate) fn openai(&self) -> &dyn OpenAi {
87 self.openai.as_ref()
88 }
89
90 pub(crate) fn api(&self) -> &dyn Api {
91 self.api.as_ref()
92 }
93
94 pub fn new(home: PathBuf) -> Context {
98 Context {
99 pitboard_home: home.join(".pitboard"),
100 home,
101 claude_config_dir: None,
102 secure_storage_dir: None,
103 user: None,
104 custom_oauth: false,
105 argv_fallback: true,
106 overriding_auth: Vec::new(),
107 caller: "unknown".into(),
108 claude_program: PathBuf::from("claude"),
109 api_base: None,
110 hover_rest: false,
111 codex_home: None,
112 codex_program: PathBuf::from("codex"),
113 search_path: None,
114 clock: Arc::new(SystemClock),
115 host: crate::store::host(),
116 api: Arc::new(Anthropic),
117 openai: Arc::new(OpenAiNetwork),
118 }
119 }
120
121 pub fn with_codex_home(mut self, dir: String) -> Context {
123 self.codex_home = Some(dir).filter(|d| !d.is_empty());
124 self
125 }
126
127 pub(crate) fn codex_home(&self) -> Option<&str> {
128 self.codex_home.as_deref()
129 }
130
131 pub fn with_pitboard_home(mut self, dir: PathBuf) -> Context {
132 self.pitboard_home = dir;
133 self
134 }
135
136 pub fn with_claude_config_dir(mut self, dir: String) -> Context {
138 self.claude_config_dir = Some(dir).filter(|d| !d.is_empty());
139 self
140 }
141
142 pub fn with_secure_storage_dir(mut self, dir: String) -> Context {
145 self.secure_storage_dir = Some(dir);
146 self
147 }
148
149 pub fn with_argv_fallback(mut self, allowed: bool) -> Context {
152 self.argv_fallback = allowed;
153 self
154 }
155
156 pub fn custom_oauth(&self) -> bool {
158 self.custom_oauth
159 }
160
161 pub fn claude_program(&self) -> &std::path::Path {
163 &self.claude_program
164 }
165
166 pub fn argv_fallback(&self) -> bool {
168 self.argv_fallback
169 }
170
171 pub fn overriding_auth(&self) -> &[String] {
173 &self.overriding_auth
174 }
175
176 pub fn with_caller(mut self, caller: String) -> Context {
178 self.caller = caller;
179 self
180 }
181
182 pub fn with_user(mut self, user: String) -> Context {
183 self.user = Some(user);
184 self
185 }
186
187 pub fn with_claude_program(mut self, program: PathBuf) -> Context {
189 self.claude_program = program;
190 self
191 }
192
193 pub fn with_codex_program(mut self, program: PathBuf) -> Context {
195 self.codex_program = program;
196 self
197 }
198
199 pub fn codex_program(&self) -> &std::path::Path {
201 &self.codex_program
202 }
203
204 pub fn with_search_path(mut self, path: String) -> Context {
209 self.search_path = Some(path.into());
210 self
211 }
212
213 pub(crate) fn search_path(&self) -> std::ffi::OsString {
215 self.search_path
216 .clone()
217 .or_else(|| std::env::var_os("PATH"))
218 .unwrap_or_default()
219 }
220
221 pub fn program_for(&self, tool: crate::provider::ProviderId) -> &std::path::Path {
223 match tool {
224 crate::provider::ProviderId::Claude => &self.claude_program,
225 crate::provider::ProviderId::Codex => &self.codex_program,
226 }
227 }
228
229 pub fn from_env() -> Context {
230 let var = |name: &str| std::env::var(name).ok();
231 let home = std::env::var_os("HOME")
232 .map(PathBuf::from)
233 .unwrap_or_default();
234 Context {
235 pitboard_home: std::env::var_os("PITBOARD_HOME")
236 .map(PathBuf::from)
237 .unwrap_or_else(|| home.join(".pitboard")),
238 home,
239 claude_config_dir: var("CLAUDE_CONFIG_DIR").filter(|v| !v.is_empty()),
240 secure_storage_dir: var("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
241 user: var("USER"),
242 custom_oauth: var("CLAUDE_CODE_CUSTOM_OAUTH_URL").is_some_and(|v| !v.is_empty()),
243 argv_fallback: !var("PITBOARD_NO_ARGV").is_some_and(|v| v == "1"),
244 overriding_auth: crate::settings::OVERRIDING_ENV
245 .iter()
246 .filter(|name| var(name).is_some_and(|v| !v.is_empty()))
247 .map(|name| (*name).to_string())
248 .collect(),
249 caller: "cli".into(),
250 claude_program: PathBuf::from("claude"),
251 api_base: var("PITBOARD_API_BASE"),
252 hover_rest: var("CLAUDE_CODE_HOVER_REST").is_some_and(|v| v == "1" || v == "true"),
253 codex_home: var("CODEX_HOME").filter(|v| !v.is_empty()),
254 codex_program: PathBuf::from("codex"),
255 search_path: None,
256 clock: Arc::new(SystemClock),
257 host: crate::store::host(),
258 api: Arc::new(Anthropic),
259 openai: Arc::new(OpenAiNetwork),
260 }
261 }
262
263 #[cfg(any(test, feature = "test-support"))]
267 #[doc(hidden)]
268 pub fn with_scripted_api(mut self, api: Arc<crate::api::scripted::ScriptedApi>) -> Context {
269 self.api = api.clone();
270 self.openai = api;
271 self
272 }
273
274 #[cfg(any(test, feature = "test-support"))]
277 #[doc(hidden)]
278 pub fn with_memory_stores(mut self, memory: Arc<crate::store::memory::MemoryHost>) -> Context {
279 self.host = memory;
280 self
281 }
282
283 #[cfg(any(test, feature = "test-support"))]
286 #[doc(hidden)]
287 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Context {
288 self.clock = clock;
289 self
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn an_explicit_context_reads_claude_codes_settings_the_way_the_environment_does() {
299 let ctx = Context::new(PathBuf::from("/home/x"))
300 .with_claude_config_dir(String::new())
301 .with_secure_storage_dir(String::new());
302 assert_eq!(ctx.pitboard_home, PathBuf::from("/home/x/.pitboard"));
303 assert_eq!(ctx.claude_config_dir, None, "empty means unset");
304 assert_eq!(
305 ctx.secure_storage_dir.as_deref(),
306 Some(""),
307 "empty is set, and pins the default slot"
308 );
309 assert_eq!(ctx.claude_program, PathBuf::from("claude"));
310 }
311}