Skip to main content

xurl_core/provider/
mod.rs

1use 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        // Precedence:
76        // 1) XDG_DATA_HOME/amp
77        // 2) ~/.local/share/amp
78        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        // Precedence:
85        // 1) COPILOT_HOME (official Copilot CLI config/data root env)
86        // 2) ~/.copilot (Copilot CLI default)
87        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        // Precedence:
93        // 1) CODEX_HOME (official Codex home env)
94        // 2) ~/.codex (Codex default)
95        let codex_root = env::var_os("CODEX_HOME")
96            .map(PathBuf::from)
97            .unwrap_or_else(|| home.join(".codex"));
98
99        // Precedence:
100        // 1) CLAUDE_CONFIG_DIR (official Claude Code config/data root env)
101        // 2) ~/.claude (Claude default)
102        let claude_root = env::var_os("CLAUDE_CONFIG_DIR")
103            .map(PathBuf::from)
104            .unwrap_or_else(|| home.join(".claude"));
105
106        // Precedence:
107        // 1) CURSOR_DATA_DIR
108        // 2) CURSOR_CONFIG_DIR
109        // 3) ~/.cursor
110        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        // Precedence:
117        // 1) GEMINI_CLI_HOME/.gemini (official Gemini CLI home env)
118        // 2) ~/.gemini (Gemini default)
119        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        // Precedence:
125        // 1) KIMI_SHARE_DIR (official Kimi share dir env)
126        // 2) ~/.kimi (Kimi default)
127        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        // Precedence:
133        // 1) PI_CODING_AGENT_DIR (official pi coding agent root env)
134        // 2) ~/.pi/agent (pi default)
135        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        // Precedence:
141        // 1) XDG_DATA_HOME/opencode
142        // 2) ~/.local/share/opencode
143        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}