Skip to main content

agent_sdk/
storage.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::path::{Path, PathBuf};
4
5use crate::config::AGENT_DIR;
6use crate::error::{SdkError, SdkResult};
7use uuid::Uuid;
8
9#[derive(Debug, Clone)]
10pub struct AgentPaths {
11    work_dir: PathBuf,
12    home_dir: PathBuf,
13    project_key: String,
14}
15
16impl AgentPaths {
17    pub fn for_work_dir(work_dir: &Path) -> SdkResult<Self> {
18        let work_dir = canonicalize_for_storage(work_dir)?;
19        let home_dir = dirs::home_dir()
20            .ok_or_else(|| SdkError::Config("Could not resolve home directory".to_string()))?
21            .join(AGENT_DIR);
22        let identity_path = git_common_dir(&work_dir).unwrap_or_else(|| work_dir.clone());
23        let project_key = project_key_for_path(&identity_path);
24
25        Ok(Self {
26            work_dir,
27            home_dir,
28            project_key,
29        })
30    }
31
32    pub fn project_config_dir(&self) -> PathBuf {
33        self.work_dir.join(AGENT_DIR)
34    }
35
36    pub fn project_settings_path(&self) -> PathBuf {
37        self.project_config_dir().join("settings.json")
38    }
39
40    pub fn project_local_settings_path(&self) -> PathBuf {
41        self.project_config_dir().join("settings.local.json")
42    }
43
44    pub fn user_root_dir(&self) -> PathBuf {
45        self.home_dir.clone()
46    }
47
48    pub fn user_settings_path(&self) -> PathBuf {
49        self.home_dir.join("settings.json")
50    }
51
52    pub fn projects_dir(&self) -> PathBuf {
53        self.home_dir.join("projects")
54    }
55
56    pub fn project_state_dir(&self) -> PathBuf {
57        self.projects_dir().join(&self.project_key)
58    }
59
60    pub fn project_tasks_dir(&self) -> PathBuf {
61        self.project_state_dir().join("tasks")
62    }
63
64    pub fn project_mailbox_dir(&self) -> PathBuf {
65        self.project_state_dir().join("mailbox")
66    }
67
68    pub fn project_memory_dir(&self) -> PathBuf {
69        self.project_state_dir().join("memory")
70    }
71
72    pub fn project_sessions_dir(&self) -> PathBuf {
73        self.project_state_dir().join("sessions")
74    }
75
76    pub fn cli_session_path(&self) -> PathBuf {
77        self.project_sessions_dir().join("cli-session.json")
78    }
79
80    pub fn project_key(&self) -> &str {
81        &self.project_key
82    }
83
84    pub fn teams_dir(&self) -> PathBuf {
85        self.home_dir.join("teams")
86    }
87
88    pub fn tasks_dir(&self) -> PathBuf {
89        self.home_dir.join("tasks")
90    }
91
92    pub fn new_team_name(&self) -> String {
93        format!("{}-{}", self.project_key, &Uuid::new_v4().to_string()[..8])
94    }
95
96    pub fn team_dir(&self, team_name: &str) -> PathBuf {
97        self.teams_dir().join(team_name)
98    }
99
100    pub fn team_config_path(&self, team_name: &str) -> PathBuf {
101        self.team_dir(team_name).join("config.json")
102    }
103
104    pub fn team_mailbox_dir(&self, team_name: &str) -> PathBuf {
105        self.team_dir(team_name).join("mailbox")
106    }
107
108    pub fn team_memory_dir(&self, team_name: &str) -> PathBuf {
109        self.team_dir(team_name).join("memory")
110    }
111
112    pub fn team_tasks_dir(&self, team_name: &str) -> PathBuf {
113        self.tasks_dir().join(team_name)
114    }
115}
116
117fn canonicalize_for_storage(path: &Path) -> SdkResult<PathBuf> {
118    if path.exists() {
119        return std::fs::canonicalize(path).map_err(SdkError::Io);
120    }
121
122    let joined = std::env::current_dir().map_err(SdkError::Io)?.join(path);
123    Ok(joined)
124}
125
126fn project_key_for_path(path: &Path) -> String {
127    let label = path
128        .file_name()
129        .and_then(|s| s.to_str())
130        .filter(|s| !s.is_empty())
131        .unwrap_or("project");
132    let slug: String = label
133        .chars()
134        .map(|c| {
135            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
136                c
137            } else {
138                '-'
139            }
140        })
141        .collect();
142
143    let mut hasher = DefaultHasher::new();
144    path.hash(&mut hasher);
145    let hash = hasher.finish();
146
147    format!("{slug}-{hash:016x}")
148}
149
150fn git_common_dir(work_dir: &Path) -> Option<PathBuf> {
151    for dir in work_dir.ancestors() {
152        let git_entry = dir.join(".git");
153
154        if git_entry.is_dir() {
155            return std::fs::canonicalize(git_entry).ok();
156        }
157
158        if git_entry.is_file() {
159            let gitdir = resolve_gitdir_from_file(dir, &git_entry)?;
160            let common = resolve_common_dir(&gitdir).unwrap_or(gitdir);
161            return std::fs::canonicalize(common).ok();
162        }
163    }
164
165    None
166}
167
168fn resolve_gitdir_from_file(base_dir: &Path, git_file: &Path) -> Option<PathBuf> {
169    let content = std::fs::read_to_string(git_file).ok()?;
170    let gitdir = content.trim().strip_prefix("gitdir:")?.trim();
171    let path = Path::new(gitdir);
172    let resolved = if path.is_absolute() {
173        path.to_path_buf()
174    } else {
175        base_dir.join(path)
176    };
177    Some(resolved)
178}
179
180fn resolve_common_dir(gitdir: &Path) -> Option<PathBuf> {
181    let commondir = gitdir.join("commondir");
182    let content = std::fs::read_to_string(commondir).ok()?;
183    let path = Path::new(content.trim());
184    Some(if path.is_absolute() {
185        path.to_path_buf()
186    } else {
187        gitdir.join(path)
188    })
189}