syncable_cli/agent/tools/platform/
list_organizations.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 ListOrganizationsArgs {}
16
17#[derive(Debug, thiserror::Error)]
19#[error("List organizations error: {0}")]
20pub struct ListOrganizationsError(String);
21
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct ListOrganizationsTool;
28
29impl ListOrganizationsTool {
30 pub fn new() -> Self {
32 Self
33 }
34}
35
36impl Tool for ListOrganizationsTool {
37 const NAME: &'static str = "list_organizations";
38
39 type Error = ListOrganizationsError;
40 type Args = ListOrganizationsArgs;
41 type Output = String;
42
43 async fn definition(&self, _prompt: String) -> ToolDefinition {
44 ToolDefinition {
45 name: Self::NAME.to_string(),
46 description: r#"List all organizations the authenticated user belongs to.
47
48Returns a list of organizations with their IDs, names, and slugs.
49Use this to discover available organizations before listing projects.
50
51**Prerequisites:**
52- User must be authenticated via `sync-ctl auth login`
53
54**Use Cases:**
55- Finding the organization ID to list projects
56- Discovering which organizations the user has access to
57- Getting organization details for project selection"#
58 .to_string(),
59 parameters: json!({
60 "type": "object",
61 "properties": {},
62 "required": []
63 }),
64 }
65 }
66
67 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
68 let client = match PlatformApiClient::new() {
70 Ok(c) => c,
71 Err(e) => {
72 return Ok(format_api_error("list_organizations", e));
73 }
74 };
75
76 match client.list_organizations().await {
78 Ok(orgs) => {
79 if orgs.is_empty() {
80 return Ok(json!({
81 "success": true,
82 "organizations": [],
83 "count": 0,
84 "message": "No organizations found. You may need to create or join an organization."
85 })
86 .to_string());
87 }
88
89 let org_list: Vec<serde_json::Value> = orgs
90 .iter()
91 .map(|org| {
92 json!({
93 "id": org.id,
94 "name": org.name,
95 "slug": org.slug,
96 "created_at": org.created_at.to_rfc3339()
97 })
98 })
99 .collect();
100
101 let result = json!({
102 "success": true,
103 "organizations": org_list,
104 "count": orgs.len()
105 });
106
107 serde_json::to_string_pretty(&result)
108 .map_err(|e| ListOrganizationsError(format!("Failed to serialize: {}", e)))
109 }
110 Err(e) => Ok(format_api_error("list_organizations", e)),
111 }
112 }
113}
114
115fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
117 match error {
118 PlatformApiError::Unauthorized => format_error_for_llm(
119 tool_name,
120 ErrorCategory::PermissionDenied,
121 "Not authenticated - please run `sync-ctl auth login` first",
122 Some(vec![
123 "The user needs to authenticate with the Syncable platform",
124 "Run: sync-ctl auth login",
125 ]),
126 ),
127 PlatformApiError::NotFound(msg) => format_error_for_llm(
128 tool_name,
129 ErrorCategory::ResourceUnavailable,
130 &format!("Resource not found: {}", msg),
131 Some(vec!["The requested resource does not exist"]),
132 ),
133 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
134 tool_name,
135 ErrorCategory::PermissionDenied,
136 &format!("Permission denied: {}", msg),
137 Some(vec!["The user does not have access to this resource"]),
138 ),
139 PlatformApiError::RateLimited => format_error_for_llm(
140 tool_name,
141 ErrorCategory::ResourceUnavailable,
142 "Rate limit exceeded - please try again later",
143 Some(vec!["Wait a moment before retrying"]),
144 ),
145 PlatformApiError::HttpError(e) => format_error_for_llm(
146 tool_name,
147 ErrorCategory::NetworkError,
148 &format!("Network error: {}", e),
149 Some(vec![
150 "Check network connectivity",
151 "The Syncable API may be temporarily unavailable",
152 ]),
153 ),
154 PlatformApiError::ParseError(msg) => format_error_for_llm(
155 tool_name,
156 ErrorCategory::InternalError,
157 &format!("Failed to parse API response: {}", msg),
158 Some(vec!["This may be a temporary API issue"]),
159 ),
160 PlatformApiError::ApiError { status, message } => format_error_for_llm(
161 tool_name,
162 ErrorCategory::ExternalCommandFailed,
163 &format!("API error ({}): {}", status, message),
164 Some(vec!["Check the error message for details"]),
165 ),
166 PlatformApiError::ServerError { status, message } => format_error_for_llm(
167 tool_name,
168 ErrorCategory::ExternalCommandFailed,
169 &format!("Server error ({}): {}", status, message),
170 Some(vec![
171 "The Syncable API is experiencing issues",
172 "Try again later",
173 ]),
174 ),
175 PlatformApiError::ConnectionFailed => format_error_for_llm(
176 tool_name,
177 ErrorCategory::NetworkError,
178 "Could not connect to Syncable API",
179 Some(vec![
180 "Check your internet connection",
181 "The Syncable API may be temporarily unavailable",
182 ]),
183 ),
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_tool_name() {
193 assert_eq!(ListOrganizationsTool::NAME, "list_organizations");
194 }
195
196 #[test]
197 fn test_tool_creation() {
198 let tool = ListOrganizationsTool::new();
199 assert!(format!("{:?}", tool).contains("ListOrganizationsTool"));
200 }
201}