import { supabase } from "./supabase"; const API_CONFIG = { local: { http: "http://localhost:8080", ws: "ws://localhost:8080", }, production: { http: "https://api.makima.jp", ws: "wss://api.makima.jp", }, } as const; type Environment = "local" | "production"; function detectEnvironment(): Environment { // Check if explicitly set via env var const envOverride = import.meta.env.VITE_API_ENV as Environment | undefined; if (envOverride && (envOverride === "local" || envOverride === "production")) { return envOverride; } // Auto-detect based on hostname if (typeof window !== "undefined") { const hostname = window.location.hostname; if (hostname === "localhost" || hostname === "127.0.0.1") { return "local"; } } return "production"; } const env = detectEnvironment(); export const API_BASE = API_CONFIG[env].http; export const WS_BASE = API_CONFIG[env].ws; // ============================================================================= // Authentication helpers // ============================================================================= /** Storage key for API key */ const API_KEY_STORAGE_KEY = "makima_api_key"; /** Get stored API key from localStorage */ export function getStoredApiKey(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(API_KEY_STORAGE_KEY); } /** Store API key in localStorage */ export function setStoredApiKey(key: string): void { if (typeof window === "undefined") return; localStorage.setItem(API_KEY_STORAGE_KEY, key); } /** Remove stored API key */ export function clearStoredApiKey(): void { if (typeof window === "undefined") return; localStorage.removeItem(API_KEY_STORAGE_KEY); } /** Get auth headers for API requests */ async function getAuthHeaders(): Promise { const headers: HeadersInit = { "Content-Type": "application/json", }; // Try Supabase session first if (supabase) { const { data: { session } } = await supabase.auth.getSession(); if (session?.access_token) { headers["Authorization"] = `Bearer ${session.access_token}`; return headers; } } // Fall back to API key if available const apiKey = getStoredApiKey(); if (apiKey) { headers["X-Makima-API-Key"] = apiKey; } return headers; } /** Fetch with authentication headers */ async function authFetch(url: string, options: RequestInit = {}): Promise { const authHeaders = await getAuthHeaders(); const mergedHeaders = { ...authHeaders, ...options.headers, }; return fetch(url, { ...options, headers: mergedHeaders, }); } export const LISTEN_ENDPOINT = `${WS_BASE}/api/v1/listen`; export const SPEAK_ENDPOINT = `${WS_BASE}/api/v1/speak`; export const FILE_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/files/subscribe`; export const TASK_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/mesh/tasks/subscribe`; export function getEnvironment(): Environment { return env; } // File API types export interface TranscriptEntry { id: string; speaker: string; start: number; end: number; text: string; isFinal: boolean; } // Chart types for visualization export type ChartType = "line" | "bar" | "pie" | "area"; // Body element types for structured content export type BodyElement = | { type: "heading"; level: number; text: string } | { type: "paragraph"; text: string } | { type: "code"; language?: string; content: string } | { type: "list"; ordered: boolean; items: string[] } | { type: "chart"; chartType: ChartType; title?: string; data: Record[]; config?: Record; } | { type: "image"; src: string; alt?: string; caption?: string } | { type: "markdown"; content: string }; export interface FileSummary { id: string; name: string; description: string | null; transcriptCount: number; duration: number | null; version: number; /** Path to linked repository file (e.g., "README.md") */ repoFilePath: string | null; /** Sync status: 'none', 'synced', 'modified', 'conflict' */ repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null; createdAt: string; updatedAt: string; // Contract info (joined from contracts table) contractId: string | null; contractName: string | null; contractPhase: ContractPhase | null; } export interface FileDetail { id: string; ownerId: string; name: string; description: string | null; transcript: TranscriptEntry[]; location: string | null; summary: string | null; body: BodyElement[]; version: number; /** Path to linked repository file (e.g., "README.md") */ repoFilePath: string | null; /** When file was last synced from repository */ repoSyncedAt: string | null; /** Sync status: 'none', 'synced', 'modified', 'conflict' */ repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null; createdAt: string; updatedAt: string; } export interface FileListResponse { files: FileSummary[]; total: number; } export interface CreateFileRequest { /** Contract this file belongs to (required - files must belong to a contract) */ contractId: string; name?: string; description?: string; transcript?: TranscriptEntry[]; location?: string; /** Initial body elements (e.g., from a template) */ body?: BodyElement[]; } export interface UpdateFileRequest { name?: string; description?: string; transcript?: TranscriptEntry[]; summary?: string; body?: BodyElement[]; version?: number; } // Conflict error types export interface ConflictErrorResponse { code: "VERSION_CONFLICT"; message: string; expectedVersion: number; actualVersion: number; } export class VersionConflictError extends Error { expectedVersion: number; actualVersion: number; constructor(conflict: ConflictErrorResponse) { super(conflict.message); this.name = "VersionConflictError"; this.expectedVersion = conflict.expectedVersion; this.actualVersion = conflict.actualVersion; } } // Available LLM models export type LlmModel = "claude-sonnet" | "claude-opus" | "groq"; // Chat API types export interface ChatMessage { role: "user" | "assistant"; content: string; } export interface ChatRequest { message: string; model?: LlmModel; history?: ChatMessage[]; focusedElementIndex?: number; } export interface ToolCallInfo { name: string; result: { success: boolean; message: string; }; } // User question types for interactive LLM tool export interface UserQuestion { id: string; question: string; options: string[]; allowMultiple: boolean; allowCustom: boolean; } export interface UserAnswer { id: string; answers: string[]; } export interface ChatResponse { response: string; toolCalls: ToolCallInfo[]; updatedBody: BodyElement[]; updatedSummary: string | null; pendingQuestions?: UserQuestion[]; } // File API functions export async function listFiles(): Promise { const res = await authFetch(`${API_BASE}/api/v1/files`); if (!res.ok) { throw new Error(`Failed to list files: ${res.statusText}`); } return res.json(); } export async function getFile(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${id}`); if (!res.ok) { throw new Error(`Failed to get file: ${res.statusText}`); } return res.json(); } export async function createFile(data: CreateFileRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/files`, { method: "POST", body: JSON.stringify(data), }); if (!res.ok) { throw new Error(`Failed to create file: ${res.statusText}`); } return res.json(); } export async function updateFile( id: string, data: UpdateFileRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, { method: "PUT", body: JSON.stringify(data), }); if (res.status === 409) { const conflict = (await res.json()) as ConflictErrorResponse; throw new VersionConflictError(conflict); } if (!res.ok) { throw new Error(`Failed to update file: ${res.statusText}`); } return res.json(); } export async function deleteFile(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, { method: "DELETE", }); if (!res.ok) { throw new Error(`Failed to delete file: ${res.statusText}`); } } // Chat API function export async function chatWithFile( id: string, message: string, model?: LlmModel, history?: ChatMessage[], focusedElementIndex?: number ): Promise { const body: ChatRequest = { message }; if (model) { body.model = model; } if (history && history.length > 0) { body.history = history; } if (focusedElementIndex !== undefined) { body.focusedElementIndex = focusedElementIndex; } const res = await authFetch(`${API_BASE}/api/v1/files/${id}/chat`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Chat failed: ${errorText || res.statusText}`); } return res.json(); } // Version history types export type VersionSource = "user" | "llm" | "system"; export interface FileVersion { version: number; name: string; description: string | null; summary: string | null; body: BodyElement[]; source: VersionSource; createdAt: string; changeDescription?: string; } export interface FileVersionSummary { version: number; source: VersionSource; createdAt: string; changeDescription?: string; } export interface FileVersionListResponse { versions: FileVersionSummary[]; total: number; } export interface RestoreVersionRequest { targetVersion: number; } // Version history API functions export async function listFileVersions(fileId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions`); if (!res.ok) { throw new Error(`Failed to list versions: ${res.statusText}`); } return res.json(); } export async function getFileVersion(fileId: string, version: number): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/${version}`); if (!res.ok) { throw new Error(`Failed to get version: ${res.statusText}`); } return res.json(); } export async function restoreFileVersion( fileId: string, targetVersion: number, currentVersion: number ): Promise { const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/restore`, { method: "POST", body: JSON.stringify({ targetVersion, currentVersion }), }); if (res.status === 409) { const conflict = (await res.json()) as ConflictErrorResponse; throw new VersionConflictError(conflict); } if (!res.ok) { throw new Error(`Failed to restore version: ${res.statusText}`); } return res.json(); } /** * Sync a file from its linked repository file. * Triggers an async operation - the file will be updated when the daemon responds. * Returns 202 Accepted if the sync started successfully. */ export async function syncFileFromRepo(fileId: string): Promise<{ message: string; fileId: string }> { const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/sync-from-repo`, { method: "POST", }); if (!res.ok) { const error = await res.json(); throw new Error(error.message || `Failed to sync file: ${res.statusText}`); } return res.json(); } // ============================================================================= // LLM Tool Definitions for Version History // ============================================================================= // These types define the tools available to the LLM for version history access. // The backend should implement handlers for these tools. /** * Tool: read_version * Allows the LLM to read the content of a specific historical version. * This is read-only - it does not modify the document. */ export interface ReadVersionToolInput { version: number; } export interface ReadVersionToolOutput { success: boolean; version: number; body: BodyElement[]; summary: string | null; source: VersionSource; createdAt: string; changeDescription?: string; message: string; } /** * Tool: list_versions * Allows the LLM to list all available versions of the document. */ export interface ListVersionsToolOutput { success: boolean; versions: FileVersionSummary[]; currentVersion: number; message: string; } /** * Tool: restore_version * Allows the LLM to restore the document to a specific historical version. * This creates a new version with the content from the target version. */ export interface RestoreVersionToolInput { targetVersion: number; reason?: string; } export interface RestoreVersionToolOutput { success: boolean; previousVersion: number; newVersion: number; restoredFromVersion: number; message: string; } // LLM Tool type definitions for the backend export type LlmVersionTool = | { name: "read_version"; input: ReadVersionToolInput } | { name: "list_versions"; input: Record } | { name: "restore_version"; input: RestoreVersionToolInput }; export type LlmVersionToolResult = | { name: "read_version"; result: ReadVersionToolOutput } | { name: "list_versions"; result: ListVersionsToolOutput } | { name: "restore_version"; result: RestoreVersionToolOutput }; // ============================================================================= // Mesh/Task Types for Claude Code Orchestration // ============================================================================= export type TaskStatus = | "pending" | "initializing" | "starting" | "running" | "paused" | "blocked" | "done" | "failed" | "merged"; export type MergeMode = "pr" | "auto" | "manual"; /** Action to perform when a task completes successfully */ export type CompletionAction = "none" | "branch" | "merge" | "pr"; export type DaemonStatus = "connected" | "disconnected" | "unhealthy"; export interface TaskSummary { id: string; /** Contract this task belongs to */ contractId: string | null; /** Contract name (joined from contracts table) */ contractName: string | null; /** Contract phase (joined from contracts table) */ contractPhase: ContractPhase | null; /** Contract status (joined from contracts table) */ contractStatus: ContractStatus | null; parentTaskId: string | null; depth: number; name: string; status: TaskStatus; priority: number; progressSummary: string | null; subtaskCount: number; /** Whether this is a supervisor task (contract orchestrator) */ isSupervisor: boolean; /** Whether this task is hidden from the UI (user dismissed it) */ hidden: boolean; version: number; createdAt: string; updatedAt: string; } export interface Task { id: string; ownerId: string; /** Contract this task belongs to */ contractId: string | null; parentTaskId: string | null; depth: number; name: string; description: string | null; status: TaskStatus; priority: number; plan: string; // Daemon/container info daemonId: string | null; containerId: string | null; overlayPath: string | null; // Repository info repositoryUrl: string | null; baseBranch: string | null; targetBranch: string | null; // Merge settings mergeMode: MergeMode | null; prUrl: string | null; // Completion action settings /** Path to user's local repository for completion actions */ targetRepoPath: string | null; /** Action on completion: "none", "branch", "merge", "pr" */ completionAction: CompletionAction | null; // Progress tracking progressSummary: string | null; lastOutput: string | null; errorMessage: string | null; // Timestamps startedAt: string | null; completedAt: string | null; version: number; createdAt: string; updatedAt: string; // Supervisor flag isSupervisor: boolean; } export interface TaskWithSubtasks extends Task { subtasks: TaskSummary[]; } export interface TaskListResponse { tasks: TaskSummary[]; total: number; } export interface CreateTaskRequest { /** Contract this task belongs to (optional - can be standalone) */ contractId?: string; name: string; description?: string; plan: string; parentTaskId?: string; priority?: number; repositoryUrl?: string; baseBranch?: string; targetBranch?: string; mergeMode?: MergeMode; /** Path to user's local repository for completion actions */ targetRepoPath?: string; /** Action on completion: "none", "branch", "merge", "pr" */ completionAction?: CompletionAction; } export interface UpdateTaskRequest { name?: string; description?: string; plan?: string; status?: TaskStatus; priority?: number; progressSummary?: string; lastOutput?: string; errorMessage?: string; mergeMode?: MergeMode; prUrl?: string; /** Path to user's local repository for completion actions */ targetRepoPath?: string; /** Action on completion: "none", "branch", "merge", "pr" */ completionAction?: CompletionAction; /** Whether this task is hidden from the UI (user dismissed it) */ hidden?: boolean; version?: number; } export interface TaskEvent { id: string; taskId: string; eventType: string; previousStatus: string | null; newStatus: string | null; eventData: Record | null; createdAt: string; } export interface TaskEventListResponse { events: TaskEvent[]; total: number; } export interface Daemon { id: string; ownerId: string; connectionId: string; hostname: string | null; machineId: string | null; maxConcurrentTasks: number; currentTaskCount: number; status: DaemonStatus; lastHeartbeatAt: string; connectedAt: string; disconnectedAt: string | null; } export interface DaemonListResponse { daemons: Daemon[]; total: number; } // Mesh API functions export async function listTasks(): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`); if (!res.ok) { throw new Error(`Failed to list tasks: ${res.statusText}`); } return res.json(); } export async function getTask(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`); if (!res.ok) { throw new Error(`Failed to get task: ${res.statusText}`); } return res.json(); } export async function createTask(data: CreateTaskRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`, { method: "POST", body: JSON.stringify(data), }); if (!res.ok) { throw new Error(`Failed to create task: ${res.statusText}`); } return res.json(); } export async function updateTask( id: string, data: UpdateTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, { method: "PUT", body: JSON.stringify(data), }); if (res.status === 409) { const conflict = (await res.json()) as ConflictErrorResponse; throw new VersionConflictError(conflict); } if (!res.ok) { throw new Error(`Failed to update task: ${res.statusText}`); } return res.json(); } export async function deleteTask(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, { method: "DELETE", }); if (!res.ok) { throw new Error(`Failed to delete task: ${res.statusText}`); } } export async function startTask(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/start`, { method: "POST", }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to start task: ${errorText || res.statusText}`); } return res.json(); } export async function stopTask(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/stop`, { method: "POST", }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to stop task: ${errorText || res.statusText}`); } return res.json(); } export interface SendMessageResponse { success: boolean; taskId: string; messageLength: number; } /** * Send a message to a running task's stdin. * This can be used to provide input to Claude Code when it's waiting for user input, * or to inject context/instructions into a running task. */ export async function sendTaskMessage( taskId: string, message: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/message`, { method: "POST", body: JSON.stringify({ message }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to send message: ${errorText || res.statusText}`); } return res.json(); } export interface RetryCompletionResponse { success: boolean; taskId: string; action: string; targetRepoPath: string; message: string; } /** * Retry completion action for a completed task. * This allows retrying a completion action (push branch, merge, create PR) * after filling in the target_repo_path if it wasn't set when the task completed. */ export async function retryCompletionAction( taskId: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/retry-completion`, { method: "POST", }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to retry completion action: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Git Actions for Tasks (Manual) // ============================================================================= /** Response from export patch */ export interface ExportPatchResponse { success: boolean; taskId: string; fileName: string; filePath?: string; patchSize?: number; message: string; } /** * Export a task's changes as a patch file. * The patch will be saved to the contract's patch directory. */ export async function exportTaskPatch( taskId: string, fileName?: string ): Promise { const body: Record = {}; if (fileName) { body.fileName = fileName; } const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/export-patch`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to export patch: ${errorText || res.statusText}`); } return res.json(); } /** Response from push branch */ export interface PushBranchResponse { success: boolean; taskId: string; branchName: string; remote?: string; message: string; } /** * Push a task's changes to a remote branch. * Creates a branch if it doesn't exist and pushes the commits. */ export async function pushTaskBranch( taskId: string, branchName?: string ): Promise { const body: Record = {}; if (branchName) { body.branchName = branchName; } const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/push-branch`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to push branch: ${errorText || res.statusText}`); } return res.json(); } /** Response from create PR */ export interface CreatePRResponse { success: boolean; taskId: string; prUrl?: string; prNumber?: number; branchName?: string; message: string; } /** * Create a pull request for a task's changes. * First pushes the branch if needed, then creates the PR. */ export async function createTaskPR( taskId: string, title?: string, body?: string ): Promise { const reqBody: Record = {}; if (title) { reqBody.title = title; } if (body) { reqBody.body = body; } const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/create-pr`, { method: "POST", body: JSON.stringify(reqBody), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to create PR: ${errorText || res.statusText}`); } return res.json(); } /** A suggested directory from a connected daemon */ export interface DaemonDirectory { /** Path to the directory */ path: string; /** Display label for the directory */ label: string; /** Type of directory: "working", "makima", "worktrees" */ directoryType: string; /** Daemon hostname this directory is from */ hostname: string | null; /** Whether the directory already exists (for validation) */ exists?: boolean; } export interface DaemonDirectoriesResponse { directories: DaemonDirectory[]; } /** * Get suggested directories from connected daemons. * These can be used as target_repo_path suggestions for completion actions. */ export async function getDaemonDirectories(): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/directories`); if (!res.ok) { throw new Error(`Failed to get daemon directories: ${res.statusText}`); } return res.json(); } /** Request to clone a worktree */ export interface CloneWorktreeRequest { targetDir: string; } /** Response from clone worktree */ export interface CloneWorktreeResponse { status: string; taskId: string; targetDir: string; } /** * Clone a task's worktree to a target directory. */ export async function cloneWorktree( taskId: string, targetDir: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/clone`, { method: "POST", body: JSON.stringify({ targetDir }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to clone worktree: ${errorText || res.statusText}`); } return res.json(); } /** Request to check if target exists */ export interface CheckTargetExistsRequest { targetDir: string; } /** Response from check target exists */ export interface CheckTargetExistsResponse { status: string; taskId: string; targetDir: string; } /** * Check if a target directory exists. */ export async function checkTargetExists( taskId: string, targetDir: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/check-target`, { method: "POST", body: JSON.stringify({ targetDir }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to check target: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Task Recovery (Daemon Failover) // ============================================================================= /** Request to reassign a task to a new daemon */ export interface ReassignTaskRequest { targetDaemonId?: string; includeContext?: boolean; } /** Response from reassigning a task */ export interface ReassignTaskResponse { task: Task; daemonId: string; oldTaskId: string; contextIncluded: boolean; contextEntries: number; } /** * Reassign a task to a new daemon after daemon disconnect. * Creates a new task with conversation context, deletes the old one. */ export async function reassignTask( taskId: string, options?: ReassignTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/reassign`, { method: "POST", body: JSON.stringify(options || {}), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to reassign task: ${errorText || res.statusText}`); } return res.json(); } /** Request to continue a task */ export interface ContinueTaskRequest { targetDaemonId?: string; } /** Response from continuing a task */ export interface ContinueTaskResponse { task: Task; daemonId: string; contextEntries: number; } /** * Continue a task after daemon disconnect by restarting it with conversation context. * Unlike reassign, this keeps the same task ID. */ export async function continueTask( taskId: string, options?: ContinueTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/continue`, { method: "POST", body: JSON.stringify(options || {}), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to continue task: ${errorText || res.statusText}`); } return res.json(); } export async function listSubtasks(taskId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/subtasks`); if (!res.ok) { throw new Error(`Failed to list subtasks: ${res.statusText}`); } return res.json(); } // ============================================================================= // Task Branching // ============================================================================= /** Request to branch a task */ export interface BranchTaskRequest { /** Message to send to the new branched task */ message: string; /** Optional name for the new task (defaults to "Branch of {original task name}") */ name?: string; /** Whether to include conversation history from the source task */ includeConversation?: boolean; } /** Response from branching a task */ export interface BranchTaskResponse { /** The newly created task */ task: Task; /** Number of conversation messages copied to the new task */ messageCount: number; /** ID of the daemon assigned to the new task (null if not yet assigned) */ daemonId: string | null; } /** * Branch a task to create a new task with the same state. * Copies the worktree and optionally the conversation history. */ export async function branchTask( taskId: string, request: BranchTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/branch`, { method: "POST", body: JSON.stringify(request), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to branch task: ${errorText || res.statusText}`); } return res.json(); } export async function listTaskEvents( taskId: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/events`); if (!res.ok) { throw new Error(`Failed to list task events: ${res.statusText}`); } return res.json(); } /** A single output entry from a Claude Code task */ export interface TaskOutputEntry { id: string; taskId: string; /** Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" */ messageType: string; /** Main text content */ content: string; /** Tool name if tool_use message */ toolName?: string; /** Tool input JSON if tool_use message */ toolInput?: Record; /** Whether tool result was an error */ isError?: boolean; /** Cost in USD if result message */ costUsd?: number; /** Duration in ms if result message */ durationMs?: number; /** Timestamp when this output was recorded */ createdAt: string; } /** Response from the task output endpoint */ export interface TaskOutputResponse { entries: TaskOutputEntry[]; total: number; taskId: string; } /** * Get task output history. * Retrieves all recorded output from a task's Claude Code process. * Use this to fetch missed output when subscribing late or reconnecting. */ export async function getTaskOutput( taskId: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/output`); if (!res.ok) { throw new Error(`Failed to get task output: ${res.statusText}`); } return res.json(); } export async function listDaemons(): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons`); if (!res.ok) { throw new Error(`Failed to list daemons: ${res.statusText}`); } return res.json(); } export async function getDaemon(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}`); if (!res.ok) { throw new Error(`Failed to get daemon: ${res.statusText}`); } return res.json(); } /** Response from the restart daemon endpoint */ export interface RestartDaemonResponse { success: boolean; daemonId: string; message: string; } /** * Restart a connected daemon. * Sends a restart command to the daemon, which will gracefully terminate * and restart. Any running tasks will be interrupted. */ export async function restartDaemon(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}/restart`, { method: "POST", }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.message || `Failed to restart daemon: ${res.statusText}`); } return res.json(); } // ============================================================================= // Daemon Platform Download // ============================================================================= /** A daemon platform with its availability and download URL */ export interface DaemonPlatform { platform: string; available: boolean; downloadUrl: string; } /** Response from the list daemon platforms endpoint */ export interface DaemonPlatformsResponse { platforms: DaemonPlatform[]; } /** * List available daemon platforms and their download status. * This is an unauthenticated endpoint. */ export async function listDaemonPlatforms(): Promise { const res = await fetch(`${API_BASE}/api/v1/daemon/download/platforms`); if (!res.ok) { throw new Error(`Failed to list daemon platforms: ${res.statusText}`); } return res.json(); } /** * Get the full download URL for a daemon binary. * Returns the absolute URL including API_BASE for cross-origin usage. */ export function getDaemonDownloadUrl(platform: string): string { return `${API_BASE}/api/v1/daemon/download/${platform}`; } // ============================================================================= // Daemon Reauthorization // ============================================================================= /** Response from the trigger daemon reauth endpoint */ export interface TriggerReauthResponse { success: boolean; daemonId: string; requestId: string; } /** Response from the reauth status polling endpoint */ export interface ReauthStatusResponse { status: string; // "pending" | "url_ready" | "completed" | "failed" loginUrl?: string; error?: string; } /** * Trigger OAuth re-authentication on a daemon. * Sends a reauth command to the daemon, which will spawn `claude setup-token` * and return the OAuth login URL. */ export async function triggerDaemonReauth(daemonId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth`, { method: "POST", }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.message || `Failed to trigger reauth: ${res.statusText}`); } return res.json(); } /** * Submit an OAuth auth code to a daemon's pending reauth flow. */ export async function submitDaemonAuthCode( daemonId: string, code: string, requestId: string, ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/code`, { method: "POST", body: JSON.stringify({ code, requestId }), }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.message || `Failed to submit auth code: ${res.statusText}`); } } /** * Get the status of a daemon reauth request. * Used for polling to track reauth flow progress. */ export async function getDaemonReauthStatus( daemonId: string, requestId: string, ): Promise { const res = await authFetch( `${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/${requestId}/status`, ); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.message || `Failed to get reauth status: ${res.statusText}`); } return res.json(); } // ============================================================================= // Mesh Chat Types for Task Orchestration // ============================================================================= export interface MeshChatMessage { role: "user" | "assistant"; content: string; } export interface MeshChatRequest { message: string; model?: LlmModel; history?: MeshChatMessage[]; } export interface MeshToolCallInfo { name: string; result: { success: boolean; message: string; }; } export interface MeshChatResponse { response: string; toolCalls: MeshToolCallInfo[]; pendingQuestions?: UserQuestion[]; } // Mesh Chat API functions // Top-level mesh chat (no specific task context) export async function chatWithMesh( message: string, model?: LlmModel, history?: MeshChatMessage[] ): Promise { const body: MeshChatRequest = { message }; if (model) { body.model = model; } if (history && history.length > 0) { body.history = history; } const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); } return res.json(); } // Task-scoped mesh chat export async function chatWithTask( taskId: string, message: string, model?: LlmModel, history?: MeshChatMessage[] ): Promise { const body: MeshChatRequest = { message }; if (model) { body.model = model; } if (history && history.length > 0) { body.history = history; } const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/chat`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Mesh Chat History Types // ============================================================================= export type MeshChatContextType = "mesh" | "task" | "subtask"; export interface MeshChatContext { type: MeshChatContextType; taskId?: string; parentTaskId?: string; } export interface MeshChatMessageRecord { id: string; conversationId: string; role: "user" | "assistant" | "error"; content: string; contextType: MeshChatContextType; contextTaskId: string | null; toolCalls: MeshToolCallInfo[] | null; pendingQuestions: UserQuestion[] | null; createdAt: string; } export interface MeshChatHistoryResponse { conversationId: string; messages: MeshChatMessageRecord[]; } export interface MeshChatWithContextRequest { message: string; model?: LlmModel; contextType?: MeshChatContextType; contextTaskId?: string; } // ============================================================================= // Mesh Chat History API Functions // ============================================================================= /** * Get the current chat history from the database */ export async function getMeshChatHistory(): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`); if (!res.ok) { throw new Error(`Failed to get chat history: ${res.statusText}`); } return res.json(); } /** * Clear chat history (archives current conversation, starts new one) */ export async function clearMeshChatHistory(): Promise<{ success: boolean; conversationId: string }> { const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`, { method: "DELETE", }); if (!res.ok) { throw new Error(`Failed to clear chat history: ${res.statusText}`); } return res.json(); } /** * Chat with mesh using context (new approach with DB history) */ export async function chatWithMeshContext( message: string, context: MeshChatContext, model?: LlmModel ): Promise { const body: MeshChatWithContextRequest = { message, contextType: context.type, }; if (model) { body.model = model; } // Set contextTaskId based on context type if (context.type === "task" && context.taskId) { body.contextTaskId = context.taskId; } else if (context.type === "subtask" && context.taskId) { body.contextTaskId = context.taskId; } // Use top-level endpoint (it now loads history from DB) const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Mesh chat failed: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // API Key Management // ============================================================================= export interface ApiKeyInfo { id: string; prefix: string; name: string | null; lastUsedAt: string | null; createdAt: string; } export interface CreateApiKeyResponse { id: string; key: string; prefix: string; name: string | null; createdAt: string; } export interface RefreshApiKeyResponse { id: string; key: string; prefix: string; name: string | null; createdAt: string; previousKeyRevoked: boolean; } export interface RevokeApiKeyResponse { message: string; revokedKeyPrefix: string; } /** * Get information about the current active API key. */ export async function getApiKey(): Promise { const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`); if (res.status === 404) { return null; } if (!res.ok) { throw new Error(`Failed to get API key: ${res.statusText}`); } return res.json(); } /** * Create a new API key. */ export async function createApiKey(name?: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, { method: "POST", body: JSON.stringify({ name }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to create API key: ${errorText || res.statusText}`); } return res.json(); } /** * Refresh (rotate) the current API key. */ export async function refreshApiKey(name?: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys/refresh`, { method: "POST", body: JSON.stringify({ name }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to refresh API key: ${errorText || res.statusText}`); } return res.json(); } /** * Revoke the current API key. */ export async function revokeApiKey(): Promise { const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, { method: "DELETE", }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to revoke API key: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // User Account Management // ============================================================================= export interface ChangePasswordRequest { currentPassword: string; newPassword: string; } export interface ChangePasswordResponse { success: boolean; message: string; } export interface ChangeEmailRequest { password: string; newEmail: string; } export interface ChangeEmailResponse { success: boolean; message: string; verificationSent: boolean; } export interface DeleteAccountRequest { password: string; confirmation: string; } export interface DeleteAccountResponse { success: boolean; message: string; } /** * Change the current user's password. * Requires current password verification. */ export async function changePassword( currentPassword: string, newPassword: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/users/me/password`, { method: "PUT", body: JSON.stringify({ currentPassword, newPassword }), }); if (!res.ok) { const errorData = await res.json().catch(() => null); const errorMessage = errorData?.message || res.statusText; throw new Error(errorMessage); } return res.json(); } /** * Change the current user's email address. * Requires password verification. */ export async function changeEmail( password: string, newEmail: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/users/me/email`, { method: "PUT", body: JSON.stringify({ password, newEmail }), }); if (!res.ok) { const errorData = await res.json().catch(() => null); const errorMessage = errorData?.message || res.statusText; throw new Error(errorMessage); } return res.json(); } /** * Delete the current user's account. * Requires password verification and email confirmation. */ export async function deleteAccount( password: string, confirmation: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/users/me`, { method: "DELETE", body: JSON.stringify({ password, confirmation }), }); if (!res.ok) { const errorData = await res.json().catch(() => null); const errorMessage = errorData?.message || res.statusText; throw new Error(errorMessage); } return res.json(); } // ============================================================================= // Contract Types for Workflow Management // ============================================================================= /** Contract type determines the workflow and required documents */ export type ContractType = "simple" | "specification" | "execute"; export type ContractPhase = "research" | "specify" | "plan" | "execute" | "review"; export type ContractStatus = "active" | "completed" | "archived"; export type RepositorySourceType = "remote" | "local" | "managed"; export type RepositoryStatus = "ready" | "pending" | "creating" | "failed"; /** Get valid phases for a contract type */ export function getValidPhases(contractType: ContractType): ContractPhase[] { if (contractType === "simple") { return ["plan", "execute"]; } if (contractType === "execute") { return ["execute"]; } return ["research", "specify", "plan", "execute", "review"]; } /** Get default initial phase for a contract type */ export function getDefaultPhase(contractType: ContractType): ContractPhase { if (contractType === "simple") return "plan"; if (contractType === "execute") return "execute"; return "research"; } // ============================================================================= // Contract Type Templates // ============================================================================= /** Contract type template returned by the API */ export interface ContractTypeTemplate { /** Template identifier (e.g., "simple", "specification") */ id: string; /** Display name */ name: string; /** Description of the contract type workflow */ description: string; /** Ordered list of phases for this contract type */ phases: ContractPhase[]; /** Default starting phase */ defaultPhase: ContractPhase; /** Whether this is a built-in type (always available) */ isBuiltin: boolean; /** Optional mapping from phase ID to display name */ phaseNames?: Record; } /** Response from list contract types endpoint */ export interface ListContractTypesResponse { contractTypes: ContractTypeTemplate[]; } /** Phase definition for custom templates */ export interface PhaseDefinition { id: string; name: string; order: number; } /** Deliverable definition for custom templates */ export interface DeliverableDefinition { id: string; name: string; priority: "required" | "recommended" | "optional"; } /** * List available contract types. * Returns built-in types only (simple, specification, execute). */ export async function listContractTypes(): Promise { const res = await authFetch(`${API_BASE}/api/v1/contract-types`); if (!res.ok) { throw new Error(`Failed to list contract types: ${res.statusText}`); } return res.json(); } export interface ContractRepository { id: string; contractId: string; name: string; repositoryUrl: string | null; localPath: string | null; sourceType: RepositorySourceType; status: RepositoryStatus; isPrimary: boolean; createdAt: string; updatedAt: string; } export interface ContractSummary { id: string; name: string; description: string | null; /** Contract type: "simple" or "specification" */ contractType: ContractType; phase: ContractPhase; status: ContractStatus; /** Supervisor task ID for contract orchestration */ supervisorTaskId: string | null; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly: boolean; fileCount: number; taskCount: number; repositoryCount: number; version: number; createdAt: string; } export interface Contract { id: string; ownerId: string; name: string; description: string | null; /** Contract type: "simple" or "specification" */ contractType: ContractType; phase: ContractPhase; status: ContractStatus; /** Supervisor task ID for contract orchestration */ supervisorTaskId: string | null; /** Whether tasks for this contract should run in autonomous loop mode */ autonomousLoop: boolean; /** Whether to wait for user confirmation before progressing to the next phase */ phaseGuard: boolean; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly: boolean; version: number; createdAt: string; updatedAt: string; } export interface ContractWithRelations extends Contract { repositories: ContractRepository[]; files: FileSummary[]; tasks: TaskSummary[]; } export interface ContractEvent { id: string; contractId: string; eventType: string; previousPhase: string | null; newPhase: string | null; eventData: Record | null; createdAt: string; } export interface ContractListResponse { contracts: ContractSummary[]; total: number; } export interface CreateContractRequest { name: string; description?: string; /** Contract type: "simple" (default), "specification", "execute", or custom template name */ contractType?: ContractType | string; /** UUID of a custom template to use. If provided, takes precedence over contractType. */ templateId?: string; /** Initial phase to start in (defaults based on contract type or template) */ initialPhase?: ContractPhase | string; /** When true, tasks won't auto-push or create PRs - use patch files instead */ localOnly?: boolean; } export interface UpdateContractRequest { name?: string; description?: string; phase?: ContractPhase; status?: ContractStatus; /** Enable or disable autonomous loop mode */ autonomousLoop?: boolean; /** Enable or disable phase guard mode */ phaseGuard?: boolean; version?: number; } export interface AddRemoteRepositoryRequest { name: string; repositoryUrl: string; isPrimary?: boolean; } export interface AddLocalRepositoryRequest { name: string; localPath: string; isPrimary?: boolean; } export interface CreateManagedRepositoryRequest { name: string; isPrimary?: boolean; } export interface ChangePhaseRequest { phase: ContractPhase; } // ============================================================================= // Contract API Functions // ============================================================================= /** * List all contracts. */ export async function listContracts(): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts`); if (!res.ok) { throw new Error(`Failed to list contracts: ${res.statusText}`); } return res.json(); } /** * Get a contract with all its relations. */ export async function getContract(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`); if (!res.ok) { throw new Error(`Failed to get contract: ${res.statusText}`); } return res.json(); } /** * Create a new contract. */ export async function createContract( data: CreateContractRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts`, { method: "POST", body: JSON.stringify(data), }); if (!res.ok) { throw new Error(`Failed to create contract: ${res.statusText}`); } return res.json(); } /** * Update a contract. */ export async function updateContract( id: string, data: UpdateContractRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, { method: "PUT", body: JSON.stringify(data), }); if (res.status === 409) { const conflict = (await res.json()) as ConflictErrorResponse; throw new VersionConflictError(conflict); } if (!res.ok) { throw new Error(`Failed to update contract: ${res.statusText}`); } return res.json(); } /** * Delete a contract. */ export async function deleteContract(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, { method: "DELETE", }); if (!res.ok) { throw new Error(`Failed to delete contract: ${res.statusText}`); } } /** * Change contract phase. * @param confirmed - When true, confirms the phase change even if phase_guard is enabled. * Defaults to true for explicit user actions like drag-and-drop. */ export async function changeContractPhase( id: string, phase: ContractPhase, confirmed: boolean = true ): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/phase`, { method: "POST", body: JSON.stringify({ phase, confirmed }), }); if (!res.ok) { throw new Error(`Failed to change phase: ${res.statusText}`); } return res.json(); } /** * Get contract event history. */ export async function getContractEvents( id: string ): Promise { const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/events`); if (!res.ok) { throw new Error(`Failed to get events: ${res.statusText}`); } return res.json(); } // ============================================================================= // Contract Repository Management // ============================================================================= /** * Add a remote repository to a contract. */ export async function addRemoteRepository( contractId: string, data: AddRemoteRepositoryRequest ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/repositories/remote`, { method: "POST", body: JSON.stringify(data), } ); if (!res.ok) { throw new Error(`Failed to add remote repository: ${res.statusText}`); } return res.json(); } /** * Add a local repository to a contract. */ export async function addLocalRepository( contractId: string, data: AddLocalRepositoryRequest ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/repositories/local`, { method: "POST", body: JSON.stringify(data), } ); if (!res.ok) { throw new Error(`Failed to add local repository: ${res.statusText}`); } return res.json(); } /** * Create a managed repository (daemon will create it). */ export async function createManagedRepository( contractId: string, data: CreateManagedRepositoryRequest ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/repositories/managed`, { method: "POST", body: JSON.stringify(data), } ); if (!res.ok) { throw new Error(`Failed to create managed repository: ${res.statusText}`); } return res.json(); } /** * Delete a repository from a contract. */ export async function deleteContractRepository( contractId: string, repoId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}`, { method: "DELETE", } ); if (!res.ok) { throw new Error(`Failed to delete repository: ${res.statusText}`); } } /** * Set a repository as primary. */ export async function setRepositoryPrimary( contractId: string, repoId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}/primary`, { method: "PUT", } ); if (!res.ok) { throw new Error(`Failed to set repository as primary: ${res.statusText}`); } } // ============================================================================= // Contract Task Association // ============================================================================= /** * Add a task to a contract. */ export async function addTaskToContract( contractId: string, taskId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`, { method: "POST", } ); if (!res.ok) { throw new Error(`Failed to add task to contract: ${res.statusText}`); } } /** * Remove a task from a contract. */ export async function removeTaskFromContract( contractId: string, taskId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`, { method: "DELETE", } ); if (!res.ok) { throw new Error(`Failed to remove task from contract: ${res.statusText}`); } } // ============================================================================= // Contract Chat Types and API // ============================================================================= export interface ContractChatRequest { message: string; model?: LlmModel; history?: ChatMessage[]; } export interface ContractToolCallInfo { name: string; result: { success: boolean; message: string; }; } export interface ContractChatResponse { response: string; toolCalls: ContractToolCallInfo[]; pendingQuestions?: UserQuestion[]; } /** * Chat with a contract using LLM-powered management tools. */ export async function chatWithContract( contractId: string, message: string, model?: LlmModel, history?: ChatMessage[] ): Promise { const body: ContractChatRequest = { message }; if (model) { body.model = model; } if (history && history.length > 0) { body.history = history; } const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/chat`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Contract chat failed: ${errorText || res.statusText}`); } return res.json(); } // Contract chat history types export interface ContractChatMessage { id: string; conversationId: string; role: "user" | "assistant" | "error"; content: string; toolCalls?: unknown; pendingQuestions?: unknown; createdAt: string; } export interface ContractChatHistoryResponse { contractId: string; conversationId: string; messages: ContractChatMessage[]; } /** Get contract chat history */ export async function getContractChatHistory( contractId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/chat/history` ); if (!res.ok) { throw new Error(`Failed to fetch contract chat history: ${res.statusText}`); } return res.json(); } /** Clear contract chat history (starts a new conversation) */ export async function clearContractChatHistory( contractId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/chat/history`, { method: "DELETE" } ); if (!res.ok) { throw new Error(`Failed to clear contract chat history: ${res.statusText}`); } } // ============================================================================= // Contract Discussion Types and API // ============================================================================= export interface DiscussContractRequest { message: string; model?: LlmModel; history?: ChatMessage[]; transcriptContext?: string; } export interface CreatedContractInfo { id: string; name: string; description: string | null; contractType: string; initialPhase: string; } export interface DiscussContractResponse { response: string; toolCalls: ContractToolCallInfo[]; createdContract?: CreatedContractInfo; pendingQuestions?: UserQuestion[]; } /** * Discuss a potential contract with Makima. * This is an ephemeral conversation that can result in contract creation. */ export async function discussContract( message: string, model?: LlmModel, history?: ChatMessage[], transcriptContext?: string ): Promise { const body: DiscussContractRequest = { message }; if (model) body.model = model; if (history && history.length > 0) body.history = history; if (transcriptContext) body.transcriptContext = transcriptContext; const res = await authFetch(`${API_BASE}/api/v1/contracts/discuss`, { method: "POST", body: JSON.stringify(body), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Discussion failed: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Template Types and API // ============================================================================= export interface TemplateSummary { id: string; name: string; phase: ContractPhase; description: string; elementCount: number; } export interface FileTemplate { id: string; name: string; phase: ContractPhase; description: string; suggestedBody: BodyElement[]; } export interface ListTemplatesResponse { templates: TemplateSummary[]; } export async function listTemplates( phase?: ContractPhase ): Promise { const params = phase ? `?phase=${phase}` : ""; const res = await authFetch(`${API_BASE}/api/v1/templates${params}`); if (!res.ok) { throw new Error(`Failed to list templates: ${res.statusText}`); } return res.json(); } export async function getTemplate(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/templates/${id}`); if (!res.ok) { throw new Error(`Failed to get template: ${res.statusText}`); } return res.json(); } // ============================================================================= // Supervisor Question Types and Functions // ============================================================================= export interface PendingQuestion { questionId: string; taskId: string; contractId: string; /** Directive this question relates to (if from a directive task) */ directiveId?: string | null; question: string; choices: string[]; context: string | null; createdAt: string; /** Whether multiple choices can be selected */ multiSelect?: boolean; /** Question type - "general" for regular questions, "phase_confirmation" for phase transitions, "contract_complete" for contract completion */ questionType?: "general" | "phase_confirmation" | "contract_complete"; /** Phase confirmation specific data (when questionType is "phase_confirmation") */ phaseConfirmation?: { currentPhase: ContractPhase; nextPhase: ContractPhase; contractName?: string; summary?: string; deliverables?: Array<{ name: string; completed: boolean; }>; }; } export interface AnswerQuestionRequest { response: string; } export interface AnswerQuestionResponse { success: boolean; } /** * Get all pending supervisor questions for the current user. */ export async function listPendingQuestions(): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/questions`); if (!res.ok) { throw new Error(`Failed to list questions: ${res.statusText}`); } return res.json(); } /** * Answer a pending supervisor question. */ export async function answerQuestion( questionId: string, response: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/mesh/questions/${questionId}/answer`, { method: "POST", body: JSON.stringify({ response }), } ); if (!res.ok) { throw new Error(`Failed to answer question: ${res.statusText}`); } return res.json(); } // ============================================================================= // Repository History Types and API // ============================================================================= export interface RepositoryHistoryEntry { id: string; ownerId: string; name: string; repositoryUrl: string | null; localPath: string | null; sourceType: "remote" | "local"; useCount: number; lastUsedAt: string; createdAt: string; } export interface RepositoryHistoryListResponse { entries: RepositoryHistoryEntry[]; total: number; } /** * List all repository history entries. * Returns entries ordered by use_count DESC, last_used_at DESC. */ export async function listRepositoryHistory(): Promise { const res = await authFetch(`${API_BASE}/api/v1/settings/repository-history`); if (!res.ok) { throw new Error(`Failed to list repository history: ${res.statusText}`); } return res.json(); } /** * Get repository suggestions based on history. * Optionally filter by source type and search query. */ export async function getRepositorySuggestions( sourceType?: "remote" | "local", query?: string, limit?: number ): Promise { const params = new URLSearchParams(); if (sourceType) params.append("source_type", sourceType); if (query) params.append("query", query); if (limit) params.append("limit", limit.toString()); const queryString = params.toString(); const url = `${API_BASE}/api/v1/settings/repository-history/suggestions${ queryString ? `?${queryString}` : "" }`; const res = await authFetch(url); if (!res.ok) { throw new Error(`Failed to get repository suggestions: ${res.statusText}`); } return res.json(); } /** * Delete a repository history entry. */ export async function deleteRepositoryHistory(id: string): Promise { const res = await authFetch( `${API_BASE}/api/v1/settings/repository-history/${id}`, { method: "DELETE", } ); if (!res.ok) { throw new Error(`Failed to delete repository history: ${res.statusText}`); } } // ============================================================================= // History Types // ============================================================================= /** History event from the timeline */ export interface HistoryEvent { id: string; ownerId: string; contractId: string | null; taskId: string | null; eventType: string; eventSubtype: string | null; phase: string | null; eventData: Record; createdAt: string; } /** Response for contract history endpoint */ export interface ContractHistoryResponse { contractId: string; entries: HistoryEvent[]; totalCount: number; cursor: string | null; } /** Tool call info in conversation messages */ export interface ToolCallInfo { id: string; name: string; input: Record; } /** Conversation message with optional tool calls */ export interface ConversationMessage { id: string; role: string; content: string; timestamp: string; toolCalls?: ToolCallInfo[]; toolName?: string; toolInput?: Record; toolResult?: string; isError?: boolean; tokenCount?: number; costUsd?: number; } /** Reference to a spawned task in supervisor conversation */ export interface TaskReference { taskId: string; taskName: string; status: string; createdAt: string; completedAt: string | null; } /** Response for supervisor conversation endpoint */ export interface SupervisorConversationResponse { contractId: string; supervisorTaskId: string; phase: string; lastActivity: string; pendingTaskIds: string[]; messages: ConversationMessage[]; spawnedTasks: TaskReference[]; } /** Response for task conversation endpoint */ export interface TaskConversationResponse { taskId: string; taskName: string; status: string; messages: ConversationMessage[]; totalTokens: number | null; totalCost: number | null; } /** Query filters for timeline endpoint */ export interface TimelineQueryFilters { contractId?: string; taskId?: string; includeSubtasks?: boolean; from?: string; to?: string; limit?: number; } /** Query filters for contract history endpoint */ export interface HistoryQueryFilters { phase?: string; eventTypes?: string; from?: string; to?: string; limit?: number; } /** Task checkpoint */ export interface TaskCheckpoint { id: string; taskId: string; checkpointNumber: number; commitSha: string; branchName: string; message: string; filesChanged: Array<{ path: string; action: string }>; linesAdded: number; linesRemoved: number; createdAt: string; } // ============================================================================= // History API Functions // ============================================================================= /** * Get contract history timeline. */ export async function getContractHistory( contractId: string, filters?: HistoryQueryFilters ): Promise { const params = new URLSearchParams(); if (filters?.phase) params.append("phase", filters.phase); if (filters?.eventTypes) params.append("event_types", filters.eventTypes); if (filters?.from) params.append("from", filters.from); if (filters?.to) params.append("to", filters.to); if (filters?.limit) params.append("limit", filters.limit.toString()); const query = params.toString(); const url = `${API_BASE}/api/v1/contracts/${contractId}/history${query ? `?${query}` : ""}`; const res = await authFetch(url); if (!res.ok) { throw new Error(`Failed to get contract history: ${res.statusText}`); } return res.json(); } /** * Get supervisor conversation history. */ export async function getSupervisorConversation( contractId: string ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation` ); if (!res.ok) { throw new Error(`Failed to get supervisor conversation: ${res.statusText}`); } return res.json(); } /** * Get task conversation history. */ export async function getTaskConversation( taskId: string, options?: { includeToolCalls?: boolean; includeToolResults?: boolean; limit?: number } ): Promise { const params = new URLSearchParams(); if (options?.includeToolCalls !== undefined) params.append("include_tool_calls", options.includeToolCalls.toString()); if (options?.includeToolResults !== undefined) params.append("include_tool_results", options.includeToolResults.toString()); if (options?.limit) params.append("limit", options.limit.toString()); const query = params.toString(); const url = `${API_BASE}/api/v1/mesh/tasks/${taskId}/conversation${query ? `?${query}` : ""}`; const res = await authFetch(url); if (!res.ok) { throw new Error(`Failed to get task conversation: ${res.statusText}`); } return res.json(); } /** * Get unified timeline for current user. */ export async function getTimeline( filters?: TimelineQueryFilters ): Promise { const params = new URLSearchParams(); if (filters?.contractId) params.append("contract_id", filters.contractId); if (filters?.taskId) params.append("task_id", filters.taskId); if (filters?.includeSubtasks !== undefined) params.append("include_subtasks", filters.includeSubtasks.toString()); if (filters?.from) params.append("from", filters.from); if (filters?.to) params.append("to", filters.to); if (filters?.limit) params.append("limit", filters.limit.toString()); const query = params.toString(); const url = `${API_BASE}/api/v1/timeline${query ? `?${query}` : ""}`; const res = await authFetch(url); if (!res.ok) { throw new Error(`Failed to get timeline: ${res.statusText}`); } return res.json(); } /** * Get task checkpoints. */ export async function getTaskCheckpoints(taskId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints`); if (!res.ok) { throw new Error(`Failed to get task checkpoints: ${res.statusText}`); } return res.json(); } // ============================================================================= // Resume/Rewind/Fork Types // ============================================================================= /** Request to rewind a task to a checkpoint */ export interface RewindTaskRequest { checkpointId?: string; checkpointSha?: string; preserveMode: "discard" | "create_branch" | "stash"; branchName?: string; } /** Response from task rewind */ export interface RewindTaskResponse { taskId: string; rewindedTo: { checkpointNumber: number; sha: string; message: string; }; preservedAs?: { stateType: string; reference: string; }; } /** Request to fork a task from a checkpoint */ export interface ForkTaskRequest { forkFromType: "checkpoint" | "timestamp" | "message_id"; forkFromValue: string; newTaskName: string; newTaskPlan: string; includeConversation?: boolean; createBranch?: boolean; branchName?: string; } /** Response from task fork */ export interface ForkTaskResponse { newTaskId: string; sourceTaskId: string; forkPoint: { forkType: string; checkpoint?: TaskCheckpoint; timestamp: string; }; branchName?: string; conversationIncluded: boolean; messageCount?: number; } /** Request to resume supervisor */ export interface ResumeSupervisorRequest { resumeMode: "continue" | "restart_phase" | "from_checkpoint"; checkpointId?: string; additionalContext?: string; } /** Response from supervisor resume */ export interface ResumeSupervisorResponse { supervisorTaskId: string; daemonId: string | null; contractId: string; phase: string; status: string; conversationMessageCount: number; } /** Request to rewind supervisor conversation */ export interface RewindConversationRequest { toMessageId?: string; byMessageCount?: number; rewindCode?: boolean; } /** Response from conversation rewind */ export interface RewindConversationResponse { contractId: string; messagesRemoved: number; newMessageCount: number; } // ============================================================================= // Resume/Rewind/Fork API Functions // ============================================================================= /** * Rewind a task to a checkpoint. */ export async function rewindTask( taskId: string, request: RewindTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/rewind`, { method: "POST", body: JSON.stringify(request), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to rewind task: ${errorText || res.statusText}`); } return res.json(); } /** * Fork a task from a checkpoint. */ export async function forkTask( taskId: string, request: ForkTaskRequest ): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/fork`, { method: "POST", body: JSON.stringify(request), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to fork task: ${errorText || res.statusText}`); } return res.json(); } /** * Resume a supervisor. */ export async function resumeSupervisor( contractId: string, request: ResumeSupervisorRequest ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/supervisor/resume`, { method: "POST", body: JSON.stringify(request), } ); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to resume supervisor: ${errorText || res.statusText}`); } return res.json(); } /** * Rewind supervisor conversation. */ export async function rewindSupervisorConversation( contractId: string, request: RewindConversationRequest ): Promise { const res = await authFetch( `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation/rewind`, { method: "POST", body: JSON.stringify(request), } ); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to rewind conversation: ${errorText || res.statusText}`); } return res.json(); } /** * Resume task from a checkpoint. */ export async function resumeFromCheckpoint( taskId: string, checkpointId: string, request: { taskName?: string; plan: string; includeConversation?: boolean } ): Promise<{ taskId: string }> { const res = await authFetch( `${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints/${checkpointId}/resume`, { method: "POST", body: JSON.stringify(request), } ); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to resume from checkpoint: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Supervisor/Autopilot Control Functions // ============================================================================= /** * Start a contract's supervisor task (enable autopilot mode). * This is a convenience wrapper around startTask. */ export async function startSupervisor(supervisorTaskId: string): Promise { return startTask(supervisorTaskId); } /** * Stop a contract's supervisor task (pause autopilot mode). * This is a convenience wrapper around stopTask. */ export async function stopSupervisor(supervisorTaskId: string): Promise { return stopTask(supervisorTaskId); } /** Status of the supervisor/autopilot for a contract */ export interface SupervisorStatus { supervisorTaskId: string | null; status: "not_configured" | "pending" | "starting" | "running" | "paused" | "done" | "failed"; daemonId: string | null; canStart: boolean; canStop: boolean; canResume: boolean; } /** * Get the supervisor status for a contract. */ export function getSupervisorStatus( contract: ContractWithRelations ): SupervisorStatus { const supervisorTaskId = contract.supervisorTaskId; if (!supervisorTaskId) { return { supervisorTaskId: null, status: "not_configured", daemonId: null, canStart: false, canStop: false, canResume: false, }; } // Find the supervisor task in the contract's tasks const supervisorTask = contract.tasks.find( (t) => t.id === supervisorTaskId && t.isSupervisor ); if (!supervisorTask) { return { supervisorTaskId, status: "pending", daemonId: null, canStart: true, canStop: false, canResume: false, }; } // Map task status to supervisor status let status: SupervisorStatus["status"]; let canStart = false; let canStop = false; let canResume = false; switch (supervisorTask.status) { case "pending": status = "pending"; canStart = true; break; case "initializing": case "starting": status = "starting"; canStop = true; break; case "running": status = "running"; canStop = true; break; case "paused": case "blocked": status = "paused"; canResume = true; canStop = true; break; case "done": case "merged": status = "done"; break; case "failed": status = "failed"; canResume = true; break; default: status = "pending"; canStart = true; } return { supervisorTaskId, status, daemonId: null, // Task summary doesn't have daemon_id, would need full task canStart, canStop, canResume, }; } // ============================================================================= // Task Dismiss (Hide) Functions // ============================================================================= /** * Dismiss (hide) a completed standalone task from the UI. * This marks the task as hidden so it won't appear in the task list. */ export async function dismissTask(taskId: string): Promise { return updateTask(taskId, { hidden: true }); } // ============================================================================= // Worktree Info Types and Functions // ============================================================================= /** File status in the worktree (git status) */ export type FileStatus = "M" | "A" | "D" | "R" | "C" | "U" | "?" | "modified" | "added" | "deleted" | "renamed" | "copied" | "unmerged" | "untracked"; /** A single changed file in the worktree */ export interface WorktreeFile { /** File path relative to worktree root */ path: string; /** Git status code (M=modified, A=added, D=deleted, R=renamed, C=copied, U=unmerged, ?=untracked) */ status: FileStatus; /** Lines added (0 if deleted or unavailable) */ linesAdded: number; /** Lines removed (0 if added or unavailable) */ linesRemoved: number; } /** Statistics about worktree changes */ export interface WorktreeStats { /** Number of files changed */ filesChanged: number; /** Total lines inserted */ insertions: number; /** Total lines deleted */ deletions: number; } /** Worktree information for a task */ export interface WorktreeInfo { /** Task ID */ taskId: string; /** Path to the worktree directory */ worktreePath: string | null; /** Whether the worktree exists on the daemon */ exists: boolean; /** Aggregate statistics */ stats: WorktreeStats; /** Changed files list */ files: WorktreeFile[]; /** Current branch name */ branch: string | null; /** Current HEAD commit SHA */ headSha: string | null; } /** * Get worktree information for a task. * Returns changed files, stats, and metadata about the worktree. */ export async function getWorktreeInfo(taskId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/worktree-info`); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to get worktree info: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Patch Types and Functions // ============================================================================= /** Summary of a patch file (contract file of type "patch") */ export interface PatchSummary { /** Patch/file ID */ id: string; /** Patch name */ name: string; /** Optional description */ description: string | null; /** Task ID this patch was created from */ taskId: string | null; /** Contract ID */ contractId: string; /** Number of files in the patch */ filesCount: number; /** Total lines added */ linesAdded: number; /** Total lines removed */ linesRemoved: number; /** List of file paths in the patch (if available) */ files: string[] | null; /** When the patch was created */ createdAt: string; /** When the patch was last updated */ updatedAt: string; } /** * List patches for a task. * Returns contract files of type "patch" associated with the task. */ export async function listTaskPatches(taskId: string, contractId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/patches?contractId=${contractId}`); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to list patches: ${errorText || res.statusText}`); } return res.json(); } // ============================================================================= // Directive Types & API // ============================================================================= export type DirectiveStatus = "draft" | "active" | "idle" | "paused" | "archived"; export type StepStatus = "pending" | "ready" | "running" | "completed" | "failed" | "skipped"; export interface Directive { id: string; ownerId: string; title: string; goal: string; status: DirectiveStatus; repositoryUrl: string | null; localPath: string | null; baseBranch: string | null; orchestratorTaskId: string | null; prUrl: string | null; prBranch: string | null; completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */ reconcileMode: string; goalUpdatedAt: string; startedAt: string | null; version: number; createdAt: string; updatedAt: string; } export interface DirectiveStep { id: string; directiveId: string; name: string; description: string | null; taskPlan: string | null; dependsOn: string[]; status: StepStatus; taskId: string | null; /** Contract ID for contract-backed steps */ contractId?: string | null; /** Contract type (e.g. "simple", "specification", "execute") for contract-backed steps */ contractType?: string | null; orderIndex: number; generation: number; startedAt: string | null; completedAt: string | null; createdAt: string; } export interface DirectiveWithSteps extends Directive { steps: DirectiveStep[]; } export interface DirectiveSummary { id: string; ownerId: string; title: string; goal: string; status: DirectiveStatus; repositoryUrl: string | null; orchestratorTaskId: string | null; prUrl: string | null; completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */ reconcileMode: string; version: number; createdAt: string; updatedAt: string; totalSteps: number; completedSteps: number; runningSteps: number; failedSteps: number; } export interface DirectiveListResponse { directives: DirectiveSummary[]; total: number; } export interface CreateDirectiveRequest { title: string; goal: string; repositoryUrl?: string; localPath?: string; baseBranch?: string; /** Enable the memory system for this directive (default: false) */ memoryEnabled?: boolean; /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) (default: auto) */ reconcileMode?: string; } export interface UpdateDirectiveRequest { title?: string; goal?: string; status?: string; repositoryUrl?: string; localPath?: string; baseBranch?: string; orchestratorTaskId?: string; /** Enable or disable the memory system for this directive */ memoryEnabled?: boolean; /** Reconcile mode: auto (timeout), semi-auto (pause), or manual (ask questions) */ reconcileMode?: string; version?: number; } export interface CreateDirectiveStepRequest { name: string; description?: string; taskPlan?: string; dependsOn?: string[]; orderIndex?: number; generation?: number; } export interface UpdateDirectiveStepRequest { name?: string; description?: string; taskPlan?: string; dependsOn?: string[]; status?: string; taskId?: string; orderIndex?: number; } export async function listDirectives(): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives`); if (!res.ok) throw new Error(`Failed to list directives: ${res.statusText}`); return res.json(); } export async function createDirective(req: CreateDirectiveRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to create directive: ${res.statusText}`); return res.json(); } export async function getDirective(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`); if (!res.ok) throw new Error(`Failed to get directive: ${res.statusText}`); return res.json(); } export async function updateDirective(id: string, req: UpdateDirectiveRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to update directive: ${res.statusText}`); return res.json(); } export async function deleteDirective(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(`Failed to delete directive: ${res.statusText}`); } export async function createDirectiveStep(directiveId: string, req: CreateDirectiveStepRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to create step: ${res.statusText}`); return res.json(); } export async function batchCreateDirectiveSteps(directiveId: string, steps: CreateDirectiveStepRequest[]): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/batch`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(steps), }); if (!res.ok) throw new Error(`Failed to batch create steps: ${res.statusText}`); return res.json(); } export async function updateDirectiveStep(directiveId: string, stepId: string, req: UpdateDirectiveStepRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to update step: ${res.statusText}`); return res.json(); } export async function deleteDirectiveStep(directiveId: string, stepId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`, { method: "DELETE" }); if (!res.ok) throw new Error(`Failed to delete step: ${res.statusText}`); } export async function startDirective(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/start`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to start directive: ${res.statusText}`); return res.json(); } export async function pauseDirective(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/pause`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to pause directive: ${res.statusText}`); return res.json(); } export async function advanceDirective(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/advance`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to advance directive: ${res.statusText}`); return res.json(); } export async function completeDirectiveStep(directiveId: string, stepId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/complete`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to complete step: ${res.statusText}`); return res.json(); } export async function failDirectiveStep(directiveId: string, stepId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/fail`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to fail step: ${res.statusText}`); return res.json(); } export async function skipDirectiveStep(directiveId: string, stepId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to skip step: ${res.statusText}`); return res.json(); } export async function updateDirectiveGoal(id: string, goal: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/goal`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ goal }), }); if (!res.ok) throw new Error(`Failed to update goal: ${res.statusText}`); return res.json(); } export async function cleanupDirective(id: string): Promise<{ message: string; taskId: string | null }> { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/cleanup`, { method: "POST", }); if (!res.ok) throw new Error(`Failed to cleanup directive: ${res.statusText}`); return res.json(); } export interface PickUpOrdersResponse { message: string; orderCount: number; taskId: string | null; } export async function createDirectivePR(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/create-pr`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to create PR: ${res.statusText}`); return res.json(); } export async function pickUpOrders(directiveId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/pick-up-orders`, { method: "POST", }); if (!res.ok) throw new Error(`Failed to pick up orders: ${res.statusText}`); return res.json(); } // ============================================================================= // Orders API // ============================================================================= export type OrderPriority = "critical" | "high" | "medium" | "low" | "none"; export type OrderStatus = "open" | "in_progress" | "under_review" | "done" | "archived"; export type OrderType = "feature" | "bug" | "spike" | "chore" | "improvement"; export interface Order { id: string; ownerId: string; title: string; description: string | null; priority: OrderPriority; status: OrderStatus; orderType: OrderType; labels: string[]; directiveId: string | null; directiveStepId: string | null; directiveName: string | null; repositoryUrl: string | null; createdAt: string; updatedAt: string; } export interface OrderListResponse { orders: Order[]; total: number; } export interface CreateOrderRequest { title: string; description?: string | null; priority?: OrderPriority; status?: OrderStatus; orderType?: OrderType; labels?: string[]; directiveId: string; repositoryUrl?: string | null; } export interface UpdateOrderRequest { title?: string; description?: string | null; priority?: OrderPriority; status?: OrderStatus; orderType?: OrderType; labels?: string[]; directiveId?: string | null; directiveStepId?: string | null; repositoryUrl?: string | null; } export async function listOrders( status?: OrderStatus, type?: OrderType, priority?: OrderPriority, directiveId?: string, search?: string, ): Promise { const params = new URLSearchParams(); if (status) params.set("status", status); if (type) params.set("type", type); if (priority) params.set("priority", priority); if (directiveId) params.set("directiveId", directiveId); if (search) params.set("search", search); const qs = params.toString(); const res = await authFetch(`${API_BASE}/api/v1/orders${qs ? `?${qs}` : ""}`); if (!res.ok) throw new Error(`Failed to list orders: ${res.statusText}`); return res.json(); } export async function createOrder(req: CreateOrderRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to create order: ${res.statusText}`); return res.json(); } export async function getOrder(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`); if (!res.ok) throw new Error(`Failed to get order: ${res.statusText}`); return res.json(); } export async function updateOrder(id: string, req: UpdateOrderRequest): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), }); if (!res.ok) throw new Error(`Failed to update order: ${res.statusText}`); return res.json(); } export async function deleteOrder(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(`Failed to delete order: ${res.statusText}`); } export async function linkOrderToDirective(orderId: string, directiveId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/link-directive`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ directiveId }), }); if (!res.ok) throw new Error(`Failed to link order to directive: ${res.statusText}`); return res.json(); } export async function convertOrderToStep(orderId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/convert-to-step`, { method: "POST", }); if (!res.ok) throw new Error(`Failed to convert order to step: ${res.statusText}`); return res.json(); }