summaryrefslogtreecommitdiff
path: root/makima/src/daemon/api
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/daemon/api
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/src/daemon/api')
-rw-r--r--makima/src/daemon/api/client.rs129
-rw-r--r--makima/src/daemon/api/contract.rs161
-rw-r--r--makima/src/daemon/api/mod.rs7
-rw-r--r--makima/src/daemon/api/supervisor.rs186
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
+ }
+}