xurl_core/provider/
mod.rs1use std::env;
2use std::path::PathBuf;
3
4use dirs::home_dir;
5
6use crate::error::{Result, XurlError};
7use crate::model::{ProviderKind, ResolvedThread, WriteRequest, WriteResult};
8
9pub mod amp;
10pub mod claude;
11pub mod codex;
12pub mod gemini;
13pub mod opencode;
14pub mod pi;
15pub mod skills;
16
17pub(crate) fn append_passthrough_args(args: &mut Vec<String>, params: &[(String, Option<String>)]) {
18 append_passthrough_args_excluding(args, params, &[]);
19}
20
21pub(crate) fn append_passthrough_args_excluding(
22 args: &mut Vec<String>,
23 params: &[(String, Option<String>)],
24 excluded_keys: &[&str],
25) -> Vec<String> {
26 let mut excluded = Vec::new();
27 for (key, value) in params {
28 if excluded_keys.iter().any(|candidate| candidate == key) {
29 excluded.push(key.clone());
30 continue;
31 }
32 args.push(format!("--{key}"));
33 if let Some(value) = value
34 && !value.is_empty()
35 {
36 args.push(value.clone());
37 }
38 }
39 excluded
40}
41
42pub trait WriteEventSink {
43 fn on_session_ready(&mut self, provider: ProviderKind, session_id: &str) -> Result<()>;
44 fn on_text_delta(&mut self, text: &str) -> Result<()>;
45}
46
47pub trait Provider {
48 fn kind(&self) -> ProviderKind;
49 fn resolve(&self, session_id: &str) -> Result<ResolvedThread>;
50 fn write(&self, req: &WriteRequest, sink: &mut dyn WriteEventSink) -> Result<WriteResult> {
51 let _ = (req, sink);
52 Err(XurlError::UnsupportedProviderWrite(self.kind().to_string()))
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ProviderRoots {
58 pub amp_root: PathBuf,
59 pub codex_root: PathBuf,
60 pub claude_root: PathBuf,
61 pub gemini_root: PathBuf,
62 pub pi_root: PathBuf,
63 pub opencode_root: PathBuf,
64 pub skills_root: PathBuf,
65 pub skills_cache_root: PathBuf,
66}
67
68impl ProviderRoots {
69 pub fn from_env_or_home() -> Result<Self> {
70 let home = home_dir().ok_or(XurlError::HomeDirectoryNotFound)?;
71
72 let amp_root = env::var_os("XDG_DATA_HOME")
76 .filter(|path| !path.is_empty())
77 .map(PathBuf::from)
78 .map(|path| path.join("amp"))
79 .unwrap_or_else(|| home.join(".local/share/amp"));
80
81 let codex_root = env::var_os("CODEX_HOME")
85 .map(PathBuf::from)
86 .unwrap_or_else(|| home.join(".codex"));
87
88 let claude_root = env::var_os("CLAUDE_CONFIG_DIR")
92 .map(PathBuf::from)
93 .unwrap_or_else(|| home.join(".claude"));
94
95 let gemini_root = env::var_os("GEMINI_CLI_HOME")
99 .map(PathBuf::from)
100 .map(|path| path.join(".gemini"))
101 .unwrap_or_else(|| home.join(".gemini"));
102
103 let pi_root = env::var_os("PI_CODING_AGENT_DIR")
107 .filter(|path| !path.is_empty())
108 .map(PathBuf::from)
109 .unwrap_or_else(|| home.join(".pi/agent"));
110
111 let opencode_root = env::var_os("XDG_DATA_HOME")
115 .filter(|path| !path.is_empty())
116 .map(PathBuf::from)
117 .map(|path| path.join("opencode"))
118 .unwrap_or_else(|| home.join(".local/share/opencode"));
119
120 let skills_root = env::var_os("XURL_SKILLS_ROOT")
124 .filter(|path| !path.is_empty())
125 .map(PathBuf::from)
126 .unwrap_or_else(|| home.join(".agents/skills"));
127
128 let skills_cache_root = env::var_os("XURL_SKILLS_CACHE_ROOT")
132 .filter(|path| !path.is_empty())
133 .map(PathBuf::from)
134 .unwrap_or_else(|| home.join(".xurl/skills"));
135
136 Ok(Self {
137 amp_root,
138 codex_root,
139 claude_root,
140 gemini_root,
141 pi_root,
142 opencode_root,
143 skills_root,
144 skills_cache_root,
145 })
146 }
147}