syncable_cli/agent/tools/platform/
list_organizations.rs

1//! List organizations tool for the agent
2//!
3//! Allows the agent to list all organizations the authenticated user belongs to.
4
5use 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/// Arguments for the list organizations tool (none required)
14#[derive(Debug, Deserialize)]
15pub struct ListOrganizationsArgs {}
16
17/// Error type for list organizations operations
18#[derive(Debug, thiserror::Error)]
19#[error("List organizations error: {0}")]
20pub struct ListOrganizationsError(String);
21
22/// Tool to list all organizations the authenticated user belongs to
23///
24/// This tool queries the Syncable Platform API to retrieve all organizations
25/// that the currently authenticated user is a member of.
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct ListOrganizationsTool;
28
29impl ListOrganizationsTool {
30    /// Create a new ListOrganizationsTool
31    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        // Create the API client
69        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        // Fetch organizations
77        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
115/// Format a PlatformApiError for LLM consumption
116fn 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}