diff options
Diffstat (limited to 'makima/src/daemon/api')
| -rw-r--r-- | makima/src/daemon/api/client.rs | 129 | ||||
| -rw-r--r-- | makima/src/daemon/api/contract.rs | 161 | ||||
| -rw-r--r-- | makima/src/daemon/api/mod.rs | 7 | ||||
| -rw-r--r-- | makima/src/daemon/api/supervisor.rs | 186 |
4 files changed, 483 insertions, 0 deletions
diff --git a/makima/src/daemon/api/client.rs b/makima/src/daemon/api/client.rs new file mode 100644 index 0000000..b27d606 --- /dev/null +++ b/makima/src/daemon/api/client.rs @@ -0,0 +1,129 @@ +//! Base HTTP client for makima API. + +use reqwest::Client; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; + +/// API client errors. +#[derive(Error, Debug)] +pub enum ApiError { + #[error("HTTP request failed: {0}")] + Request(#[from] reqwest::Error), + + #[error("API error (HTTP {status}): {message}")] + Api { status: u16, message: String }, + + #[error("Failed to parse response: {0}")] + Parse(String), +} + +/// HTTP client for makima API. +pub struct ApiClient { + client: Client, + base_url: String, + api_key: String, +} + +impl ApiClient { + /// Create a new API client. + pub fn new(base_url: String, api_key: String) -> Result<Self, ApiError> { + let client = Client::builder() + .build()?; + + Ok(Self { + client, + base_url: base_url.trim_end_matches('/').to_string(), + api_key, + }) + } + + /// Make a GET request. + pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> { + let url = format!("{}{}", self.base_url, path); + let response = self.client + .get(&url) + .header("X-Makima-Tool-Key", &self.api_key) + .send() + .await?; + + self.handle_response(response).await + } + + /// Make a POST request with JSON body. + pub async fn post<T: DeserializeOwned, B: Serialize>( + &self, + path: &str, + body: &B, + ) -> Result<T, ApiError> { + let url = format!("{}{}", self.base_url, path); + let response = self.client + .post(&url) + .header("X-Makima-Tool-Key", &self.api_key) + .header("Content-Type", "application/json") + .json(body) + .send() + .await?; + + self.handle_response(response).await + } + + /// Make a POST request without body. + pub async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> { + let url = format!("{}{}", self.base_url, path); + let response = self.client + .post(&url) + .header("X-Makima-Tool-Key", &self.api_key) + .send() + .await?; + + self.handle_response(response).await + } + + /// Make a PUT request with JSON body. + pub async fn put<T: DeserializeOwned, B: Serialize>( + &self, + path: &str, + body: &B, + ) -> Result<T, ApiError> { + let url = format!("{}{}", self.base_url, path); + let response = self.client + .put(&url) + .header("X-Makima-Tool-Key", &self.api_key) + .header("Content-Type", "application/json") + .json(body) + .send() + .await?; + + self.handle_response(response).await + } + + /// Handle API response. + async fn handle_response<T: DeserializeOwned>( + &self, + response: reqwest::Response, + ) -> Result<T, ApiError> { + let status = response.status(); + let status_code = status.as_u16(); + + if !status.is_success() { + let body = response.text().await.unwrap_or_default(); + return Err(ApiError::Api { + status: status_code, + message: body, + }); + } + + let body = response.text().await?; + + // Handle empty responses + if body.is_empty() || body == "null" { + // Try to parse empty/null as the target type + serde_json::from_str::<T>("null") + .or_else(|_| serde_json::from_str::<T>("{}")) + .map_err(|e| ApiError::Parse(e.to_string())) + } else { + serde_json::from_str::<T>(&body) + .map_err(|e| ApiError::Parse(format!("{}: {}", e, body))) + } + } +} diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs new file mode 100644 index 0000000..aac6b94 --- /dev/null +++ b/makima/src/daemon/api/contract.rs @@ -0,0 +1,161 @@ +//! Contract API methods. + +use serde::Serialize; +use uuid::Uuid; + +use super::client::{ApiClient, ApiError}; +use super::supervisor::JsonValue; + +// Request types + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportRequest { + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub task_id: Option<Uuid>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionActionRequest { + pub lines_added: i32, + pub lines_removed: i32, + pub has_code_changes: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub task_id: Option<Uuid>, + #[serde(skip_serializing_if = "Option::is_none")] + pub files_modified: Option<Vec<String>>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateFileRequest { + pub content: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateFileRequest { + pub name: String, + pub content: String, +} + +impl ApiClient { + /// Get contract status. + pub async fn contract_status(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/contracts/{}/daemon/status", contract_id)) + .await + } + + /// Get phase checklist. + pub async fn contract_checklist(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/contracts/{}/daemon/checklist", contract_id)) + .await + } + + /// Get contract goals. + pub async fn contract_goals(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/contracts/{}/daemon/goals", contract_id)) + .await + } + + /// List contract files. + pub async fn contract_files(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/contracts/{}/daemon/files", contract_id)) + .await + } + + /// Get a specific file. + pub async fn contract_file( + &self, + contract_id: Uuid, + file_id: Uuid, + ) -> Result<JsonValue, ApiError> { + self.get(&format!( + "/api/v1/contracts/{}/daemon/files/{}", + contract_id, file_id + )) + .await + } + + /// Report progress. + pub async fn contract_report( + &self, + contract_id: Uuid, + message: &str, + task_id: Option<Uuid>, + ) -> Result<JsonValue, ApiError> { + let req = ReportRequest { + message: message.to_string(), + task_id, + }; + self.post(&format!("/api/v1/contracts/{}/daemon/report", contract_id), &req) + .await + } + + /// Get suggested action. + pub async fn contract_suggest_action(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.post_empty(&format!( + "/api/v1/contracts/{}/daemon/suggest-action", + contract_id + )) + .await + } + + /// Get completion action recommendation. + pub async fn contract_completion_action( + &self, + contract_id: Uuid, + task_id: Option<Uuid>, + files_modified: Option<Vec<String>>, + lines_added: i32, + lines_removed: i32, + has_code_changes: bool, + ) -> Result<JsonValue, ApiError> { + let req = CompletionActionRequest { + task_id, + files_modified, + lines_added, + lines_removed, + has_code_changes, + }; + self.post( + &format!("/api/v1/contracts/{}/daemon/completion-action", contract_id), + &req, + ) + .await + } + + /// Update a file. + pub async fn contract_update_file( + &self, + contract_id: Uuid, + file_id: Uuid, + content: &str, + ) -> Result<JsonValue, ApiError> { + let req = UpdateFileRequest { + content: content.to_string(), + }; + self.put( + &format!("/api/v1/contracts/{}/daemon/files/{}", contract_id, file_id), + &req, + ) + .await + } + + /// Create a new file. + pub async fn contract_create_file( + &self, + contract_id: Uuid, + name: &str, + content: &str, + ) -> Result<JsonValue, ApiError> { + let req = CreateFileRequest { + name: name.to_string(), + content: content.to_string(), + }; + self.post(&format!("/api/v1/contracts/{}/daemon/files", contract_id), &req) + .await + } +} diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs new file mode 100644 index 0000000..0c05fb4 --- /dev/null +++ b/makima/src/daemon/api/mod.rs @@ -0,0 +1,7 @@ +//! HTTP API client for makima CLI commands. + +pub mod client; +pub mod contract; +pub mod supervisor; + +pub use client::ApiClient; diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs new file mode 100644 index 0000000..b691cc4 --- /dev/null +++ b/makima/src/daemon/api/supervisor.rs @@ -0,0 +1,186 @@ +//! Supervisor API methods. + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::client::{ApiClient, ApiError}; + +// Request/Response types + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SpawnTaskRequest { + pub name: String, + pub plan: String, + pub contract_id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_task_id: Option<Uuid>, + #[serde(skip_serializing_if = "Option::is_none")] + pub checkpoint_sha: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WaitRequest { + pub timeout_seconds: i32, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadFileRequest { + pub file_path: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateBranchRequest { + pub branch_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub from_ref: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MergeRequest { + pub squash: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_branch: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePrRequest { + pub task_id: Uuid, + pub title: String, + pub body: String, + pub base_branch: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckpointRequest { + pub message: String, +} + +// Generic response type for JSON output +#[derive(Deserialize, Serialize)] +pub struct JsonValue(pub serde_json::Value); + +impl ApiClient { + /// Get all tasks in a contract. + pub async fn supervisor_tasks(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/mesh/supervisor/contracts/{}/tasks", contract_id)) + .await + } + + /// Get task tree structure. + pub async fn supervisor_tree(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/mesh/supervisor/contracts/{}/tree", contract_id)) + .await + } + + /// Spawn a new task. + pub async fn supervisor_spawn(&self, req: SpawnTaskRequest) -> Result<JsonValue, ApiError> { + self.post("/api/v1/mesh/supervisor/tasks", &req).await + } + + /// Wait for a task to complete. + pub async fn supervisor_wait( + &self, + task_id: Uuid, + timeout_seconds: i32, + ) -> Result<JsonValue, ApiError> { + let req = WaitRequest { timeout_seconds }; + self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/wait", task_id), &req) + .await + } + + /// Read a file from a task's worktree. + pub async fn supervisor_read_file( + &self, + task_id: Uuid, + file_path: &str, + ) -> Result<JsonValue, ApiError> { + let req = ReadFileRequest { + file_path: file_path.to_string(), + }; + self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/read-file", task_id), &req) + .await + } + + /// Create a new branch. + pub async fn supervisor_branch( + &self, + branch_name: &str, + from_ref: Option<String>, + ) -> Result<JsonValue, ApiError> { + let req = CreateBranchRequest { + branch_name: branch_name.to_string(), + from_ref, + }; + self.post("/api/v1/mesh/supervisor/branches", &req).await + } + + /// Merge a task's changes. + pub async fn supervisor_merge( + &self, + task_id: Uuid, + target_branch: Option<String>, + squash: bool, + ) -> Result<JsonValue, ApiError> { + let req = MergeRequest { + squash, + target_branch, + }; + self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/merge", task_id), &req) + .await + } + + /// Create a pull request. + pub async fn supervisor_pr( + &self, + task_id: Uuid, + title: &str, + body: &str, + base_branch: &str, + ) -> Result<JsonValue, ApiError> { + let req = CreatePrRequest { + task_id, + title: title.to_string(), + body: body.to_string(), + base_branch: base_branch.to_string(), + }; + self.post("/api/v1/mesh/supervisor/pr", &req).await + } + + /// Get task diff. + pub async fn supervisor_diff(&self, task_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/mesh/supervisor/tasks/{}/diff", task_id)) + .await + } + + /// Create a checkpoint. + pub async fn supervisor_checkpoint( + &self, + task_id: Uuid, + message: &str, + ) -> Result<JsonValue, ApiError> { + let req = CheckpointRequest { + message: message.to_string(), + }; + self.post(&format!("/api/v1/mesh/tasks/{}/checkpoint", task_id), &req) + .await + } + + /// List checkpoints. + pub async fn supervisor_checkpoints(&self, task_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/mesh/tasks/{}/checkpoints", task_id)) + .await + } + + /// Get contract status. + pub async fn supervisor_status(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/contracts/{}/daemon/status", contract_id)) + .await + } +} |
