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}