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 copilot;
13pub mod cursor;
14pub mod gemini;
15pub mod kimi;
16pub mod opencode;
17pub mod pi;
18
19pub(crate) fn append_passthrough_args(args: &mut Vec<String>, params: &[(String, Option<String>)]) {
20 append_passthrough_args_excluding(args, params, &[]);
21}
22
23pub(crate) fn append_passthrough_args_excluding(
24 args: &mut Vec<String>,
25 params: &[(String, Option<String>)],
26 excluded_keys: &[&str],
27) -> Vec<String> {
28 let mut excluded = Vec::new();
29 for (key, value) in params {
30 if excluded_keys.iter().any(|candidate| candidate == key) {
31 excluded.push(key.clone());
32 continue;
33 }
34 args.push(format!("--{key}"));
35 if let Some(value) = value
36 && !value.is_empty()
37 {
38 args.push(value.clone());
39 }
40 }
41 excluded
42}
43
44pub trait WriteEventSink {
45 fn on_session_ready(&mut self, provider: ProviderKind, session_id: &str) -> Result<()>;
46 fn on_text_delta(&mut self, text: &str) -> Result<()>;
47}
48
49pub trait Provider {
50 fn kind(&self) -> ProviderKind;
51 fn resolve(&self, session_id: &str) -> Result<ResolvedThread>;
52 fn write(&self, req: &WriteRequest, sink: &mut dyn WriteEventSink) -> Result<WriteResult> {
53 let _ = (req, sink);
54 Err(XurlError::UnsupportedProviderWrite(self.kind().to_string()))
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ProviderRoots {
60 pub amp_root: PathBuf,
61 pub copilot_root: PathBuf,
62 pub codex_root: PathBuf,
63 pub claude_root: PathBuf,
64 pub cursor_root: PathBuf,
65 pub gemini_root: PathBuf,
66 pub kimi_root: PathBuf,
67 pub pi_root: PathBuf,
68 pub opencode_root: PathBuf,
69}
70
71impl ProviderRoots {
72 pub fn from_env_or_home() -> Result<Self> {
73 let home = home_dir().ok_or(XurlError::HomeDirectoryNotFound)?;
74
75 let amp_root = env::var_os("XDG_DATA_HOME")
79 .filter(|path| !path.is_empty())
80 .map(PathBuf::from)
81 .map(|path| path.join("amp"))
82 .unwrap_or_else(|| home.join(".local/share/amp"));
83
84 let copilot_root = env::var_os("COPILOT_HOME")
88 .filter(|path| !path.is_empty())
89 .map(PathBuf::from)
90 .unwrap_or_else(|| home.join(".copilot"));
91
92 let codex_root = env::var_os("CODEX_HOME")
96 .map(PathBuf::from)
97 .unwrap_or_else(|| home.join(".codex"));
98
99 let claude_root = env::var_os("CLAUDE_CONFIG_DIR")
103 .map(PathBuf::from)
104 .unwrap_or_else(|| home.join(".claude"));
105
106 let cursor_root = env::var_os("CURSOR_DATA_DIR")
111 .filter(|path| !path.is_empty())
112 .or_else(|| env::var_os("CURSOR_CONFIG_DIR").filter(|path| !path.is_empty()))
113 .map(PathBuf::from)
114 .unwrap_or_else(|| home.join(".cursor"));
115
116 let gemini_root = env::var_os("GEMINI_CLI_HOME")
120 .map(PathBuf::from)
121 .map(|path| path.join(".gemini"))
122 .unwrap_or_else(|| home.join(".gemini"));
123
124 let kimi_root = env::var_os("KIMI_SHARE_DIR")
128 .filter(|path| !path.is_empty())
129 .map(PathBuf::from)
130 .unwrap_or_else(|| home.join(".kimi"));
131
132 let pi_root = env::var_os("PI_CODING_AGENT_DIR")
136 .filter(|path| !path.is_empty())
137 .map(PathBuf::from)
138 .unwrap_or_else(|| home.join(".pi/agent"));
139
140 let opencode_root = env::var_os("XDG_DATA_HOME")
144 .filter(|path| !path.is_empty())
145 .map(PathBuf::from)
146 .map(|path| path.join("opencode"))
147 .unwrap_or_else(|| home.join(".local/share/opencode"));
148
149 Ok(Self {
150 amp_root,
151 copilot_root,
152 codex_root,
153 claude_root,
154 cursor_root,
155 gemini_root,
156 kimi_root,
157 pi_root,
158 opencode_root,
159 })
160 }
161}