syncable_cli/agent/tools/platform/
list_projects.rs1use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11use crate::platform::api::{PlatformApiClient, PlatformApiError};
12
13#[derive(Debug, Deserialize)]
15pub struct ListProjectsArgs {
16 pub organization_id: String,
18}
19
20#[derive(Debug, thiserror::Error)]
22#[error("List projects error: {0}")]
23pub struct ListProjectsError(String);
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct ListProjectsTool;
31
32impl ListProjectsTool {
33 pub fn new() -> Self {
35 Self
36 }
37}
38
39impl Tool for ListProjectsTool {
40 const NAME: &'static str = "list_projects";
41
42 type Error = ListProjectsError;
43 type Args = ListProjectsArgs;
44 type Output = String;
45
46 async fn definition(&self, _prompt: String) -> ToolDefinition {
47 ToolDefinition {
48 name: Self::NAME.to_string(),
49 description: r#"List all projects within an organization.
50
51Returns a list of projects with their IDs, names, and descriptions.
52Use this after getting organization IDs from list_organizations.
53
54**Prerequisites:**
55- User must be authenticated via `sync-ctl auth login`
56- User must have access to the specified organization
57
58**Use Cases:**
59- Finding project IDs to select a project context
60- Discovering available projects in an organization
61- Getting project details before selection"#
62 .to_string(),
63 parameters: json!({
64 "type": "object",
65 "properties": {
66 "organization_id": {
67 "type": "string",
68 "description": "The UUID of the organization to list projects for"
69 }
70 },
71 "required": ["organization_id"]
72 }),
73 }
74 }
75
76 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
77 if args.organization_id.trim().is_empty() {
79 return Ok(format_error_for_llm(
80 "list_projects",
81 ErrorCategory::ValidationFailed,
82 "organization_id cannot be empty",
83 Some(vec![
84 "Use list_organizations to find valid organization IDs",
85 "Pass the organization ID as a UUID string",
86 ]),
87 ));
88 }
89
90 let client = match PlatformApiClient::new() {
92 Ok(c) => c,
93 Err(e) => {
94 return Ok(format_api_error("list_projects", e));
95 }
96 };
97
98 match client.list_projects(&args.organization_id).await {
100 Ok(projects) => {
101 if projects.is_empty() {
102 return Ok(json!({
103 "success": true,
104 "organization_id": args.organization_id,
105 "projects": [],
106 "count": 0,
107 "message": "No projects found in this organization. You may need to create a project."
108 })
109 .to_string());
110 }
111
112 let project_list: Vec<serde_json::Value> = projects
113 .iter()
114 .map(|proj| {
115 json!({
116 "id": proj.id,
117 "name": proj.name,
118 "description": proj.description,
119 "organization_id": proj.organization_id,
120 "created_at": proj.created_at.to_rfc3339()
121 })
122 })
123 .collect();
124
125 let result = json!({
126 "success": true,
127 "organization_id": args.organization_id,
128 "projects": project_list,
129 "count": projects.len()
130 });
131
132 serde_json::to_string_pretty(&result)
133 .map_err(|e| ListProjectsError(format!("Failed to serialize: {}", e)))
134 }
135 Err(e) => Ok(format_api_error("list_projects", e)),
136 }
137 }
138}
139
140fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
142 match error {
143 PlatformApiError::Unauthorized => format_error_for_llm(
144 tool_name,
145 ErrorCategory::PermissionDenied,
146 "Not authenticated - please run `sync-ctl auth login` first",
147 Some(vec![
148 "The user needs to authenticate with the Syncable platform",
149 "Run: sync-ctl auth login",
150 ]),
151 ),
152 PlatformApiError::NotFound(msg) => format_error_for_llm(
153 tool_name,
154 ErrorCategory::ResourceUnavailable,
155 &format!("Organization not found: {}", msg),
156 Some(vec![
157 "The organization ID may be incorrect",
158 "Use list_organizations to find valid organization IDs",
159 ]),
160 ),
161 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
162 tool_name,
163 ErrorCategory::PermissionDenied,
164 &format!("Permission denied: {}", msg),
165 Some(vec![
166 "The user does not have access to this organization",
167 "Contact the organization admin for access",
168 ]),
169 ),
170 PlatformApiError::RateLimited => format_error_for_llm(
171 tool_name,
172 ErrorCategory::ResourceUnavailable,
173 "Rate limit exceeded - please try again later",
174 Some(vec!["Wait a moment before retrying"]),
175 ),
176 PlatformApiError::HttpError(e) => format_error_for_llm(
177 tool_name,
178 ErrorCategory::NetworkError,
179 &format!("Network error: {}", e),
180 Some(vec![
181 "Check network connectivity",
182 "The Syncable API may be temporarily unavailable",
183 ]),
184 ),
185 PlatformApiError::ParseError(msg) => format_error_for_llm(
186 tool_name,
187 ErrorCategory::InternalError,
188 &format!("Failed to parse API response: {}", msg),
189 Some(vec!["This may be a temporary API issue"]),
190 ),
191 PlatformApiError::ApiError { status, message } => format_error_for_llm(
192 tool_name,
193 ErrorCategory::ExternalCommandFailed,
194 &format!("API error ({}): {}", status, message),
195 Some(vec!["Check the error message for details"]),
196 ),
197 PlatformApiError::ServerError { status, message } => format_error_for_llm(
198 tool_name,
199 ErrorCategory::ExternalCommandFailed,
200 &format!("Server error ({}): {}", status, message),
201 Some(vec![
202 "The Syncable API is experiencing issues",
203 "Try again later",
204 ]),
205 ),
206 PlatformApiError::ConnectionFailed => format_error_for_llm(
207 tool_name,
208 ErrorCategory::NetworkError,
209 "Could not connect to Syncable API",
210 Some(vec![
211 "Check your internet connection",
212 "The Syncable API may be temporarily unavailable",
213 ]),
214 ),
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_tool_name() {
224 assert_eq!(ListProjectsTool::NAME, "list_projects");
225 }
226
227 #[test]
228 fn test_tool_creation() {
229 let tool = ListProjectsTool::new();
230 assert!(format!("{:?}", tool).contains("ListProjectsTool"));
231 }
232}