gitent_sdk/
lib.rs

1//! # gitent-sdk
2//!
3//! SDK for AI agents to integrate with gitent version control.
4//!
5//! ## Example
6//!
7//! ```no_run
8//! use gitent_sdk::GitentClient;
9//!
10//! let client = GitentClient::new("http://localhost:3030", "my-agent");
11//!
12//! // Announce a file write
13//! client.file_written("src/main.rs", "fn main() {}", None).unwrap();
14//!
15//! // Commit changes
16//! client.commit("Implemented main function").unwrap();
17//! ```
18
19use anyhow::Result;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23#[derive(Clone)]
24pub struct GitentClient {
25    base_url: String,
26    agent_id: String,
27    client: reqwest::blocking::Client,
28}
29
30#[derive(Serialize)]
31struct CreateChangeRequest {
32    change_type: String,
33    path: String,
34    content_before: Option<String>,
35    content_after: Option<String>,
36    agent_id: Option<String>,
37}
38
39#[derive(Serialize)]
40struct CreateCommitRequest {
41    message: String,
42    agent_id: String,
43    change_ids: Vec<String>,
44}
45
46#[derive(Deserialize)]
47struct Change {
48    id: String,
49}
50
51impl GitentClient {
52    /// Create a new gitent client
53    ///
54    /// # Arguments
55    ///
56    /// * `base_url` - Base URL of the gitent server (e.g., "http://localhost:3030")
57    /// * `agent_id` - Unique identifier for this agent
58    pub fn new(base_url: impl Into<String>, agent_id: impl Into<String>) -> Self {
59        Self {
60            base_url: base_url.into(),
61            agent_id: agent_id.into(),
62            client: reqwest::blocking::Client::new(),
63        }
64    }
65
66    /// Announce that a file was created
67    pub fn file_created(&self, path: &str, content: &str) -> Result<()> {
68        self.create_change("create", path, None, Some(content))
69    }
70
71    /// Announce that a file was modified
72    pub fn file_modified(
73        &self,
74        path: &str,
75        content_before: &str,
76        content_after: &str,
77    ) -> Result<()> {
78        self.create_change("modify", path, Some(content_before), Some(content_after))
79    }
80
81    /// Announce that a file was written (create or modify)
82    pub fn file_written(
83        &self,
84        path: &str,
85        content: &str,
86        previous_content: Option<&str>,
87    ) -> Result<()> {
88        if let Some(prev) = previous_content {
89            self.file_modified(path, prev, content)
90        } else {
91            self.file_created(path, content)
92        }
93    }
94
95    /// Announce that a file was deleted
96    pub fn file_deleted(&self, path: &str, content_before: Option<&str>) -> Result<()> {
97        self.create_change("delete", path, content_before, None)
98    }
99
100    fn create_change(
101        &self,
102        change_type: &str,
103        path: &str,
104        content_before: Option<&str>,
105        content_after: Option<&str>,
106    ) -> Result<()> {
107        let request = CreateChangeRequest {
108            change_type: change_type.to_string(),
109            path: path.to_string(),
110            content_before: content_before.map(|s| s.to_string()),
111            content_after: content_after.map(|s| s.to_string()),
112            agent_id: Some(self.agent_id.clone()),
113        };
114
115        self.client
116            .post(format!("{}/changes", self.base_url))
117            .json(&request)
118            .send()?
119            .error_for_status()?;
120
121        Ok(())
122    }
123
124    /// Get all uncommitted changes
125    pub fn get_uncommitted_changes(&self) -> Result<Vec<HashMap<String, serde_json::Value>>> {
126        let response = self
127            .client
128            .get(format!("{}/changes", self.base_url))
129            .send()?
130            .error_for_status()?;
131
132        Ok(response.json()?)
133    }
134
135    /// Commit all uncommitted changes
136    pub fn commit(&self, message: &str) -> Result<String> {
137        // Get uncommitted changes
138        let changes: Vec<Change> = self
139            .client
140            .get(format!("{}/changes", self.base_url))
141            .send()?
142            .error_for_status()?
143            .json()?;
144
145        let change_ids: Vec<String> = changes.iter().map(|c| c.id.clone()).collect();
146
147        let request = CreateCommitRequest {
148            message: message.to_string(),
149            agent_id: self.agent_id.clone(),
150            change_ids,
151        };
152
153        let response: serde_json::Value = self
154            .client
155            .post(format!("{}/commits", self.base_url))
156            .json(&request)
157            .send()?
158            .error_for_status()?
159            .json()?;
160
161        Ok(response["id"].as_str().unwrap_or("unknown").to_string())
162    }
163
164    /// Get commit history
165    pub fn get_commits(&self) -> Result<Vec<HashMap<String, serde_json::Value>>> {
166        let response = self
167            .client
168            .get(format!("{}/commits", self.base_url))
169            .send()?
170            .error_for_status()?;
171
172        Ok(response.json()?)
173    }
174
175    /// Check server health
176    pub fn health_check(&self) -> Result<bool> {
177        let response = self
178            .client
179            .get(format!("{}/health", self.base_url))
180            .send()?;
181
182        Ok(response.status().is_success())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_client_creation() {
192        let client = GitentClient::new("http://localhost:3030", "test-agent");
193        assert_eq!(client.base_url, "http://localhost:3030");
194        assert_eq!(client.agent_id, "test-agent");
195    }
196}