//! Contract API methods. use serde::{Deserialize, 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, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub files_modified: Option>, } #[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, } /// Request to update a contract. #[derive(Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct UpdateContractRequest { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, } /// Request to create a new contract. #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CreateContractRequest { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contract_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub initial_phase: Option, #[serde(skip_serializing_if = "Option::is_none")] pub autonomous_loop: Option, #[serde(skip_serializing_if = "Option::is_none")] pub phase_guard: Option, #[serde(skip_serializing_if = "Option::is_none")] pub local_only: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auto_merge_local: Option, #[serde(skip_serializing_if = "Option::is_none")] pub red_team_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub red_team_prompt: Option, } impl ApiClient { /// List all contracts for the authenticated user. pub async fn list_contracts(&self) -> Result { self.get("/api/v1/contracts").await } /// Create a new contract. pub async fn create_contract(&self, req: CreateContractRequest) -> Result { self.post("/api/v1/contracts", &req).await } /// Get a contract with its tasks, files, and repositories. pub async fn get_contract(&self, contract_id: Uuid) -> Result { self.get(&format!("/api/v1/contracts/{}", contract_id)).await } /// Delete a contract. pub async fn delete_contract(&self, contract_id: Uuid) -> Result<(), ApiError> { self.delete(&format!("/api/v1/contracts/{}", contract_id)) .await } /// Update a contract. pub async fn update_contract( &self, contract_id: Uuid, name: Option, description: Option, ) -> Result { let req = UpdateContractRequest { name, description }; self.put(&format!("/api/v1/contracts/{}", contract_id), &req) .await } /// Get contract status. pub async fn contract_status(&self, contract_id: Uuid) -> Result { self.get(&format!("/api/v1/contracts/{}/daemon/status", contract_id)) .await } /// Get phase checklist. pub async fn contract_checklist(&self, contract_id: Uuid) -> Result { self.get(&format!("/api/v1/contracts/{}/daemon/checklist", contract_id)) .await } /// Get contract goals. pub async fn contract_goals(&self, contract_id: Uuid) -> Result { self.get(&format!("/api/v1/contracts/{}/daemon/goals", contract_id)) .await } /// List contract files. pub async fn contract_files(&self, contract_id: Uuid) -> Result { 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 { 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, ) -> Result { 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 { 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, files_modified: Option>, lines_added: i32, lines_removed: i32, has_code_changes: bool, ) -> Result { 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 { 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 { let req = CreateFileRequest { name: name.to_string(), content: content.to_string(), }; self.post(&format!("/api/v1/contracts/{}/daemon/files", contract_id), &req) .await } /// Get task output history. pub async fn get_task_output(&self, task_id: Uuid) -> Result { self.get(&format!("/api/v1/mesh/tasks/{}/output", task_id)) .await } /// Get repository suggestions for autocomplete. /// Returns recently used repositories sorted by usage frequency and recency. pub async fn get_repository_suggestions( &self, source_type: Option<&str>, limit: Option, ) -> Result { let mut params = Vec::new(); if let Some(st) = source_type { params.push(format!("source_type={}", st)); } if let Some(l) = limit { params.push(format!("limit={}", l)); } let query_string = if params.is_empty() { String::new() } else { format!("?{}", params.join("&")) }; self.get(&format!("/api/v1/settings/repository-history/suggestions{}", query_string)) .await } /// Add a remote repository to a contract. pub async fn add_remote_repository( &self, contract_id: Uuid, name: &str, repository_url: &str, is_primary: bool, ) -> Result { let req = AddRemoteRepositoryRequest { name: name.to_string(), repository_url: repository_url.to_string(), is_primary, }; self.post(&format!("/api/v1/contracts/{}/repositories/remote", contract_id), &req) .await } } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct AddRemoteRepositoryRequest { name: String, repository_url: String, is_primary: bool, } // ============================================================================ // Contracts cleanup types // ============================================================================ /// Request for batch contract operations (cleanup, archive, etc.). #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct BatchOperationRequest { /// The operation to perform: "archive", "delete_archived", "cleanup_worktrees" pub operation: String, /// Age threshold in seconds (for archive and delete operations) #[serde(skip_serializing_if = "Option::is_none")] pub older_than_seconds: Option, /// Status filter for the operation #[serde(skip_serializing_if = "Option::is_none")] pub status_filter: Option>, /// If true, only show what would be affected without making changes #[serde(skip_serializing_if = "Option::is_none")] pub dry_run: Option, } /// Response from a batch operation. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BatchOperationResponse { /// The operation that was performed pub operation: String, /// Number of items affected pub affected_count: usize, /// IDs of affected items #[serde(default)] pub affected_ids: Vec, /// Any errors that occurred #[serde(default)] pub errors: Vec, /// Whether this was a dry run #[serde(default)] pub dry_run: bool, } /// Summary of contracts that would be affected by cleanup. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CleanupPreviewResponse { /// Contracts that would be archived #[serde(default)] pub to_archive: Vec, /// Archived contracts that would be deleted #[serde(default)] pub to_delete: Vec, /// Orphaned worktrees that would be cleaned up #[serde(default)] pub orphaned_worktrees: Vec, } /// Brief summary of a contract for cleanup operations. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContractSummary { pub id: Uuid, pub name: String, pub status: String, #[serde(default)] pub updated_at: Option, } impl ApiClient { // ======================================================================== // Contracts cleanup operations // ======================================================================== /// Preview cleanup operations (dry run). /// Returns what would be affected by archive, delete, and worktree cleanup. pub async fn cleanup_preview( &self, older_than_seconds: u64, archive: bool, delete_archived: bool, worktrees: bool, ) -> Result { let mut params = vec![ format!("older_than_seconds={}", older_than_seconds), "dry_run=true".to_string(), ]; if archive { params.push("archive=true".to_string()); } if delete_archived { params.push("delete_archived=true".to_string()); } if worktrees { params.push("worktrees=true".to_string()); } let query = params.join("&"); let result: JsonValue = self.get(&format!("/api/v1/contracts/cleanup?{}", query)).await?; serde_json::from_value(result.0) .map_err(|e| ApiError::Parse(format!("Failed to parse cleanup preview: {}", e))) } /// Archive completed/failed contracts older than the threshold. pub async fn archive_contracts( &self, older_than_seconds: u64, dry_run: bool, ) -> Result { let req = BatchOperationRequest { operation: "archive".to_string(), older_than_seconds: Some(older_than_seconds), status_filter: Some(vec!["completed".to_string(), "failed".to_string()]), dry_run: Some(dry_run), }; let result: JsonValue = self.post("/api/v1/contracts/batch", &req).await?; serde_json::from_value(result.0) .map_err(|e| ApiError::Parse(format!("Failed to parse archive response: {}", e))) } /// Delete archived contracts older than the threshold. pub async fn delete_archived_contracts( &self, older_than_seconds: u64, dry_run: bool, ) -> Result { let req = BatchOperationRequest { operation: "delete_archived".to_string(), older_than_seconds: Some(older_than_seconds), status_filter: Some(vec!["archived".to_string()]), dry_run: Some(dry_run), }; let result: JsonValue = self.post("/api/v1/contracts/batch", &req).await?; serde_json::from_value(result.0) .map_err(|e| ApiError::Parse(format!("Failed to parse delete response: {}", e))) } /// Clean up orphaned worktrees. pub async fn cleanup_worktrees(&self, dry_run: bool) -> Result { let req = BatchOperationRequest { operation: "cleanup_worktrees".to_string(), older_than_seconds: None, status_filter: None, dry_run: Some(dry_run), }; let result: JsonValue = self.post("/api/v1/contracts/batch", &req).await?; serde_json::from_value(result.0) .map_err(|e| ApiError::Parse(format!("Failed to parse worktree cleanup response: {}", e))) } /// List contracts with filtering options. pub async fn list_contracts_filtered( &self, status: Option<&str>, phase: Option<&str>, stale: bool, stale_threshold_seconds: Option, ) -> Result { let mut params = Vec::new(); if let Some(s) = status { params.push(format!("status={}", s)); } if let Some(p) = phase { params.push(format!("phase={}", p)); } if stale { params.push("stale=true".to_string()); if let Some(threshold) = stale_threshold_seconds { params.push(format!("stale_threshold={}", threshold)); } } let query_string = if params.is_empty() { String::new() } else { format!("?{}", params.join("&")) }; self.get(&format!("/api/v1/contracts{}", query_string)).await } }