difflore_core/infra/
env.rs1use std::ffi::OsString;
10use std::sync::OnceLock;
11
12pub const OPENAI_API_KEY: &str = "OPENAI_API_KEY";
15pub const ANTHROPIC_API_KEY: &str = "ANTHROPIC_API_KEY";
16
17pub const DIFFLORE_FIX_DEBUG: &str = "DIFFLORE_FIX_DEBUG";
18pub const DIFFLORE_FIX_DUMP_DIR: &str = "DIFFLORE_FIX_DUMP_DIR";
19pub const DIFFLORE_FIX_PREVIEW_REVIEW_TIMEOUT_SECS: &str =
20 "DIFFLORE_FIX_PREVIEW_REVIEW_TIMEOUT_SECS";
21pub const DIFFLORE_TRACE_HOOK: &str = "DIFFLORE_TRACE_HOOK";
22pub const DIFFLORE_HOOK_CACHE_TTL_MS: &str = "DIFFLORE_HOOK_CACHE_TTL_MS";
23pub const DIFFLORE_HOOK_SHORT_CIRCUIT: &str = "DIFFLORE_HOOK_SHORT_CIRCUIT";
38pub const DIFFLORE_HOOK_CLIENT: &str = "DIFFLORE_HOOK_CLIENT";
39pub const DIFFLORE_HOOK_FORWARD: &str = "DIFFLORE_HOOK_FORWARD";
40pub const DIFFLORE_DEBUG_HOOKS: &str = "DIFFLORE_DEBUG_HOOKS";
41pub const DIFFLORE_HOOK_SHIM_TRACE: &str = "DIFFLORE_HOOK_SHIM_TRACE";
42pub const DIFFLORE_HOOK_CROSS_REPO_STARTER: &str = "DIFFLORE_HOOK_CROSS_REPO_STARTER";
48pub const DIFFLORE_MASTER_KEY: &str = "DIFFLORE_MASTER_KEY";
49pub const DIFFLORE_HOME: &str = "DIFFLORE_HOME";
50pub const DIFFLORE_MCP_HOME: &str = "DIFFLORE_MCP_HOME";
51pub const DIFFLORE_NO_WELCOME: &str = "DIFFLORE_NO_WELCOME";
52pub const DIFFLORE_CLOUD_TOKEN: &str = "DIFFLORE_CLOUD_TOKEN";
53pub const DIFFLORE_EMBEDDING_KEY: &str = "DIFFLORE_EMBEDDING_KEY";
57pub const DIFFLORE_TOKEN: &str = "DIFFLORE_TOKEN";
58pub const DIFFLORE_DEBUG_CLOUD: &str = "DIFFLORE_DEBUG_CLOUD";
59pub const DIFFLORE_DEBUG_TELEMETRY: &str = "DIFFLORE_DEBUG_TELEMETRY";
60pub const DIFFLORE_DEBUG_PROVIDERS: &str = "DIFFLORE_DEBUG_PROVIDERS";
61pub const DIFFLORE_BFS_RETRIEVAL: &str = "DIFFLORE_BFS_RETRIEVAL";
62pub const DIFFLORE_INTENT_RERANK: &str = "DIFFLORE_INTENT_RERANK";
63pub const DIFFLORE_DISABLE_RULES: &str = "DIFFLORE_DISABLE_RULES";
64pub const DIFFLORE_DEEP_RECALL_SAMPLE_RATE: &str = "DIFFLORE_DEEP_RECALL_SAMPLE_RATE";
68pub const DIFFLORE_CLAUDE_HOME: &str = "DIFFLORE_CLAUDE_HOME";
69pub const DIFFLORE_CLOUD_URL: &str = "DIFFLORE_CLOUD_URL";
70pub const DIFF_LORE_CLOUD_URL: &str = "DIFF_LORE_CLOUD_URL";
71
72pub const NO_COLOR: &str = "NO_COLOR";
73pub const COLORTERM: &str = "COLORTERM";
74pub const TERM: &str = "TERM";
75pub const PATH: &str = "PATH";
76pub const COLORFGBG: &str = "COLORFGBG";
77pub const DIFFLORE_THEME: &str = "DIFFLORE_THEME";
78
79#[must_use]
82pub fn var(name: &str) -> Option<String> {
83 std::env::var(name).ok()
84}
85
86#[must_use]
87pub fn var_os(name: &str) -> Option<OsString> {
88 std::env::var_os(name)
89}
90
91#[must_use]
92pub fn flag_set(name: &str) -> bool {
93 std::env::var_os(name).is_some()
94}
95
96#[must_use]
97pub fn non_empty(name: &str) -> Option<String> {
98 std::env::var(name).ok().filter(|v| !v.is_empty())
99}
100
101#[must_use]
102pub fn truthy(name: &str) -> bool {
103 matches!(std::env::var(name), Ok(v) if !v.is_empty() && v != "0" && v != "false")
104}
105
106#[must_use]
109pub fn fix_debug() -> bool {
110 static CACHED: OnceLock<bool> = OnceLock::new();
111 *CACHED.get_or_init(|| flag_set(DIFFLORE_FIX_DEBUG))
112}
113
114#[must_use]
115pub fn trace_hook() -> bool {
116 static CACHED: OnceLock<bool> = OnceLock::new();
117 *CACHED.get_or_init(|| flag_set(DIFFLORE_TRACE_HOOK))
118}
119
120#[must_use]
126pub fn hook_cross_repo_starter_enabled() -> bool {
127 static CACHED: OnceLock<bool> = OnceLock::new();
128 *CACHED.get_or_init(|| truthy(DIFFLORE_HOOK_CROSS_REPO_STARTER))
129}
130
131#[must_use]
132pub fn debug_cloud() -> bool {
133 static CACHED: OnceLock<bool> = OnceLock::new();
134 *CACHED.get_or_init(|| flag_set(DIFFLORE_DEBUG_CLOUD))
135}
136
137#[must_use]
138pub fn debug_telemetry() -> bool {
139 static CACHED: OnceLock<bool> = OnceLock::new();
140 *CACHED.get_or_init(|| flag_set(DIFFLORE_DEBUG_TELEMETRY))
141}
142
143#[must_use]
144pub fn debug_providers() -> bool {
145 static CACHED: OnceLock<bool> = OnceLock::new();
146 *CACHED.get_or_init(|| flag_set(DIFFLORE_DEBUG_PROVIDERS))
147}
148
149#[must_use]
150pub fn hook_cache_ttl_ms() -> Option<u64> {
151 var(DIFFLORE_HOOK_CACHE_TTL_MS).and_then(|v| v.parse().ok())
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum HookShortCircuitMode {
159 Auto,
162 Off,
164 Force,
166}
167
168impl HookShortCircuitMode {
169 #[must_use]
170 pub fn parse(raw: &str) -> Self {
171 match raw.trim().to_ascii_lowercase().as_str() {
172 "off" | "disable" | "disabled" | "0" | "false" => Self::Off,
173 "force" | "always" => Self::Force,
174 _ => Self::Auto,
175 }
176 }
177}
178
179#[must_use]
185pub fn hook_short_circuit_mode() -> HookShortCircuitMode {
186 match var(DIFFLORE_HOOK_SHORT_CIRCUIT) {
187 Some(v) if !v.is_empty() => HookShortCircuitMode::parse(&v),
188 _ => HookShortCircuitMode::Auto,
189 }
190}
191
192pub const DEFAULT_DEEP_RECALL_SAMPLE_RATE: f32 = 0.02;
194
195pub const MAX_DEEP_RECALL_SAMPLE_RATE: f32 = 0.10;
200
201pub fn parse_deep_recall_sample_rate(raw: &str) -> Result<f32, String> {
215 let trimmed = raw.trim();
216 if trimmed.is_empty() {
217 return Err(format!(
218 "{DIFFLORE_DEEP_RECALL_SAMPLE_RATE} is empty; unset it to use the default \
219 ({DEFAULT_DEEP_RECALL_SAMPLE_RATE}) or pass a value in \
220 [0.0, {MAX_DEEP_RECALL_SAMPLE_RATE}]"
221 ));
222 }
223 let parsed: f32 = trimmed.parse().map_err(|_| {
224 format!(
225 "{DIFFLORE_DEEP_RECALL_SAMPLE_RATE}={raw:?} is not a valid f32; expected a \
226 number in [0.0, {MAX_DEEP_RECALL_SAMPLE_RATE}]"
227 )
228 })?;
229 if !parsed.is_finite() {
230 return Err(format!(
231 "{DIFFLORE_DEEP_RECALL_SAMPLE_RATE}={raw:?} must be finite; got {parsed}"
232 ));
233 }
234 if !(0.0..=MAX_DEEP_RECALL_SAMPLE_RATE).contains(&parsed) {
235 return Err(format!(
236 "{DIFFLORE_DEEP_RECALL_SAMPLE_RATE}={parsed} is out of range; expected \
237 [0.0, {MAX_DEEP_RECALL_SAMPLE_RATE}]"
238 ));
239 }
240 Ok(parsed)
241}
242
243#[must_use]
254pub fn deep_recall_sample_rate() -> f32 {
255 match var(DIFFLORE_DEEP_RECALL_SAMPLE_RATE) {
256 Some(raw) => match parse_deep_recall_sample_rate(&raw) {
257 Ok(rate) => rate,
258 Err(msg) => {
259 eprintln!(
260 "[difflore] invalid {DIFFLORE_DEEP_RECALL_SAMPLE_RATE}: {msg}; \
261 falling back to default {DEFAULT_DEEP_RECALL_SAMPLE_RATE}"
262 );
263 DEFAULT_DEEP_RECALL_SAMPLE_RATE
264 }
265 },
266 None => DEFAULT_DEEP_RECALL_SAMPLE_RATE,
267 }
268}
269
270#[must_use]
271pub fn master_key_hex() -> Option<String> {
272 var(DIFFLORE_MASTER_KEY)
273}
274
275#[must_use]
276pub fn difflore_home() -> Option<OsString> {
277 var_os(DIFFLORE_HOME)
278}
279
280#[must_use]
281pub fn fix_dump_dir() -> Option<String> {
282 var(DIFFLORE_FIX_DUMP_DIR)
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn parse_deep_recall_sample_rate_accepts_in_range_values() {
291 for raw in ["0.0", "0.02", "0.05", "0.10", " 0.02 "] {
292 let parsed = parse_deep_recall_sample_rate(raw)
293 .unwrap_or_else(|e| panic!("expected {raw:?} to parse, got error: {e}"));
294 assert!(
295 (0.0..=MAX_DEEP_RECALL_SAMPLE_RATE).contains(&parsed),
296 "{raw:?} parsed to {parsed}, outside the permitted range"
297 );
298 }
299 assert!((parse_deep_recall_sample_rate("0.02").unwrap() - 0.02).abs() < 1e-6);
301 assert!((parse_deep_recall_sample_rate("0.0").unwrap()).abs() < 1e-6);
302 }
303
304 #[test]
305 fn hook_short_circuit_mode_parses_each_state() {
306 assert_eq!(
307 HookShortCircuitMode::parse("auto"),
308 HookShortCircuitMode::Auto
309 );
310 assert_eq!(HookShortCircuitMode::parse(""), HookShortCircuitMode::Auto);
311 assert_eq!(
312 HookShortCircuitMode::parse("AUTO"),
313 HookShortCircuitMode::Auto
314 );
315 assert_eq!(
316 HookShortCircuitMode::parse("off"),
317 HookShortCircuitMode::Off
318 );
319 assert_eq!(
320 HookShortCircuitMode::parse("OFF"),
321 HookShortCircuitMode::Off
322 );
323 assert_eq!(
324 HookShortCircuitMode::parse("disabled"),
325 HookShortCircuitMode::Off
326 );
327 assert_eq!(HookShortCircuitMode::parse("0"), HookShortCircuitMode::Off);
328 assert_eq!(
329 HookShortCircuitMode::parse("false"),
330 HookShortCircuitMode::Off
331 );
332 assert_eq!(
333 HookShortCircuitMode::parse("force"),
334 HookShortCircuitMode::Force
335 );
336 assert_eq!(
337 HookShortCircuitMode::parse("FORCE"),
338 HookShortCircuitMode::Force
339 );
340 assert_eq!(
341 HookShortCircuitMode::parse("always"),
342 HookShortCircuitMode::Force
343 );
344 assert_eq!(
347 HookShortCircuitMode::parse("yolo"),
348 HookShortCircuitMode::Auto
349 );
350 }
351
352 #[test]
353 fn parse_deep_recall_sample_rate_rejects_out_of_range_and_garbage() {
354 assert!(parse_deep_recall_sample_rate("0.5").is_err());
356 assert!(parse_deep_recall_sample_rate("1.0").is_err());
357 assert!(parse_deep_recall_sample_rate("-0.01").is_err());
359 assert!(parse_deep_recall_sample_rate("two").is_err());
361 assert!(parse_deep_recall_sample_rate("").is_err());
363 assert!(parse_deep_recall_sample_rate(" ").is_err());
364 assert!(parse_deep_recall_sample_rate("NaN").is_err());
366 assert!(parse_deep_recall_sample_rate("inf").is_err());
367 }
368}