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<HeadersInit> {
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<Response> {
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<string, unknown>[];
config?: Record<string, unknown>;
}
| { 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<FileListResponse> {
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<FileDetail> {
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<FileDetail> {
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<FileDetail> {
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<void> {
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<ChatResponse> {
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<FileVersionListResponse> {
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<FileVersion> {
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<FileDetail> {
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<string, never> }
| { 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<string, unknown> | 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<TaskListResponse> {
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<TaskWithSubtasks> {
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<Task> {
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<Task> {
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<void> {
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<Task> {
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<Task> {
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<SendMessageResponse> {
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<RetryCompletionResponse> {
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<ExportPatchResponse> {
const body: Record<string, unknown> = {};
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<PushBranchResponse> {
const body: Record<string, unknown> = {};
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<CreatePRResponse> {
const reqBody: Record<string, unknown> = {};
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<DaemonDirectoriesResponse> {
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<CloneWorktreeResponse> {
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<CheckTargetExistsResponse> {
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<ReassignTaskResponse> {
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<ContinueTaskResponse> {
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<TaskListResponse> {
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<BranchTaskResponse> {
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<TaskEventListResponse> {
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<string, unknown>;
/** 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<TaskOutputResponse> {
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<DaemonListResponse> {
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<Daemon> {
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<RestartDaemonResponse> {
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<DaemonPlatformsResponse> {
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<TriggerReauthResponse> {
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<void> {
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<ReauthStatusResponse> {
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<MeshChatResponse> {
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<MeshChatResponse> {
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<MeshChatHistoryResponse> {
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<MeshChatResponse> {
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<ApiKeyInfo | null> {
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<CreateApiKeyResponse> {
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<RefreshApiKeyResponse> {
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<RevokeApiKeyResponse> {
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<ChangePasswordResponse> {
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<ChangeEmailResponse> {
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<DeleteAccountResponse> {
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();
}
// =============================================================================
// User Settings (per-user feature flags)
// =============================================================================
/** Per-user settings / feature flags. */
export interface UserSettings {
/** Whether the new "document mode" UI is enabled for this user. */
documentModeEnabled: boolean;
}
/** Request body for updating user settings. */
export interface UpdateUserSettingsRequest {
documentModeEnabled: boolean;
}
/**
* Get the authenticated user's settings (feature flags).
*/
export async function getUserSettings(): Promise<UserSettings> {
const res = await authFetch(`${API_BASE}/api/v1/users/me/settings`);
if (!res.ok) {
const errorData = await res.json().catch(() => null);
const errorMessage = errorData?.message || res.statusText;
throw new Error(errorMessage);
}
return res.json();
}
/**
* Replace the authenticated user's settings (feature flags).
*/
export async function updateUserSettings(
req: UpdateUserSettingsRequest
): Promise<UserSettings> {
const res = await authFetch(`${API_BASE}/api/v1/users/me/settings`, {
method: "PUT",
body: JSON.stringify(req),
});
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<string, string>;
}
/** 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<ListContractTypesResponse> {
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<string, unknown> | 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<ContractListResponse> {
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<ContractWithRelations> {
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<ContractSummary> {
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<ContractSummary> {
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<void> {
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<ContractSummary> {
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<ContractEvent[]> {
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<ContractRepository> {
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<ContractRepository> {
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<ContractRepository> {
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<void> {
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<void> {
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<void> {
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<void> {
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<ContractChatResponse> {
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<ContractChatHistoryResponse> {
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<void> {
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<DiscussContractResponse> {
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<ListTemplatesResponse> {
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<FileTemplate> {
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<PendingQuestion[]> {
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<AnswerQuestionResponse> {
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<RepositoryHistoryListResponse> {
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<RepositoryHistoryListResponse> {
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<void> {
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<string, unknown>;
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<string, unknown>;
}
/** Conversation message with optional tool calls */
export interface ConversationMessage {
id: string;
role: string;
content: string;
timestamp: string;
toolCalls?: ToolCallInfo[];
toolName?: string;
toolInput?: Record<string, unknown>;
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<ContractHistoryResponse> {
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<SupervisorConversationResponse> {
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<TaskConversationResponse> {
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<ContractHistoryResponse> {
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<TaskCheckpoint[]> {
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<RewindTaskResponse> {
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<ForkTaskResponse> {
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<ResumeSupervisorResponse> {
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<RewindConversationResponse> {
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<Task> {
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<Task> {
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<Task> {
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<WorktreeInfo> {
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();
}
/** Get the diff for a task's worktree changes */
export async function getTaskDiff(taskId: string): Promise<{ taskId: string; success: boolean; diff: string | null; error: string | null }> {
const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/diff`);
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Failed to get task diff: ${errorText || res.statusText}`);
}
return res.json();
}
/** Get the worktree diff for a task, optionally scoped to a single file. */
export async function getWorktreeDiff(
taskId: string,
_filePath?: string,
): Promise<{ diff: string }> {
const result = await getTaskDiff(taskId);
return { diff: result.diff ?? "" };
}
/** Commit changes in a task's worktree */
export async function commitWorktree(taskId: string, message?: string): Promise<{ taskId: string; success: boolean; commitSha: string | null; error: string | null }> {
const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/worktree-commit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Failed to commit worktree: ${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<PatchSummary[]> {
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"
| "inactive"
| "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<DirectiveListResponse> {
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<Directive> {
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<DirectiveWithSteps> {
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<Directive> {
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<void> {
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<DirectiveStep> {
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<DirectiveStep[]> {
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<DirectiveStep> {
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<void> {
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<DirectiveWithSteps> {
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<Directive> {
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<DirectiveWithSteps> {
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<DirectiveStep> {
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<DirectiveStep> {
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<DirectiveStep> {
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<Directive> {
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;
}
/**
* Per-PR snapshot of a directive's goal — frozen at PR creation, lifecycle
* tracked alongside the PR itself.
*/
export interface DirectiveRevision {
id: string;
directiveId: string;
/** Inline-markdown content of the directive goal at the moment the PR was raised. */
content: string;
prUrl: string;
prBranch: string | null;
/** "open" | "merged" | "closed" — tracks the PR lifecycle. */
prState: string;
version: number;
frozenAt: string;
}
export interface DirectiveRevisionListResponse {
revisions: DirectiveRevision[];
total: number;
}
export async function listDirectiveRevisions(
directiveId: string,
): Promise<DirectiveRevisionListResponse> {
const res = await authFetch(
`${API_BASE}/api/v1/directives/${directiveId}/revisions`,
);
if (!res.ok) {
throw new Error(`Failed to list revisions: ${res.statusText}`);
}
return res.json();
}
/**
* Reset a directive for a new draft cycle: clears its goal and detaches the
* current PR linkage. Past revisions remain attached as history. Used by the
* sidebar's "New draft" right-click on an inactive contract.
*/
export async function newDirectiveDraft(
directiveId: string,
): Promise<Directive> {
const res = await authFetch(
`${API_BASE}/api/v1/directives/${directiveId}/new-draft`,
{ method: "POST" },
);
if (!res.ok) {
throw new Error(`Failed to reset directive for new draft: ${res.statusText}`);
}
return res.json();
}
/**
* Request body for spawning an ephemeral task under a directive. The task is
* not part of the DAG — it lives alongside the directive's planned steps but
* the orchestrator ignores it.
*/
export interface CreateDirectiveTaskRequest {
name: string;
plan: string;
/** Override the directive's repository_url; defaults to the directive's. */
repositoryUrl?: string;
/** Override the directive's base_branch; defaults to the directive's. */
baseBranch?: string;
}
/**
* List ephemeral tasks (directive_id set, directive_step_id NULL) attached
* to a directive. Backs the "spinoff" group inside each directive folder
* in the document-mode sidebar.
*/
export async function listDirectiveEphemeralTasks(
directiveId: string,
): Promise<TaskListResponse> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/tasks`);
if (!res.ok) {
throw new Error(`Failed to list directive tasks: ${res.statusText}`);
}
return res.json();
}
export async function createDirectiveTask(
directiveId: string,
req: CreateDirectiveTaskRequest,
): Promise<Task> {
const res = await authFetch(
`${API_BASE}/api/v1/directives/${directiveId}/tasks`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
},
);
if (!res.ok) {
throw new Error(`Failed to create directive task: ${res.statusText}`);
}
return res.json();
}
/**
* List tasks not attached to any directive (and not subtasks). Backs the
* `tmp/` pseudo-folder in the document-mode sidebar.
*/
export async function listOrphanTasks(): Promise<TaskListResponse> {
const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks?orphan=true`);
if (!res.ok) {
throw new Error(`Failed to list orphan tasks: ${res.statusText}`);
}
return res.json();
}
export async function createDirectivePR(id: string): Promise<DirectiveWithSteps> {
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<PickUpOrdersResponse> {
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();
}
// =============================================================================
// Directive Order Groups (DOGs) API
// =============================================================================
export type DOGStatus = "open" | "in_progress" | "done" | "archived";
export interface DirectiveOrderGroup {
id: string;
directiveId: string;
ownerId: string;
name: string;
description: string | null;
status: DOGStatus;
createdAt: string;
updatedAt: string;
}
export interface DOGListResponse {
dogs: DirectiveOrderGroup[];
}
export interface CreateDOGRequest {
name: string;
description?: string | null;
}
export interface UpdateDOGRequest {
name?: string;
description?: string | null;
status?: DOGStatus;
}
export async function listDogs(directiveId: string): Promise<DOGListResponse> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`);
if (!res.ok) throw new Error(`Failed to list DOGs: ${res.statusText}`);
return res.json();
}
export async function createDog(directiveId: string, req: CreateDOGRequest): Promise<DirectiveOrderGroup> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
if (!res.ok) throw new Error(`Failed to create DOG: ${res.statusText}`);
return res.json();
}
export async function getDog(directiveId: string, dogId: string): Promise<DirectiveOrderGroup> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`);
if (!res.ok) throw new Error(`Failed to get DOG: ${res.statusText}`);
return res.json();
}
export async function updateDog(directiveId: string, dogId: string, req: UpdateDOGRequest): Promise<DirectiveOrderGroup> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
if (!res.ok) throw new Error(`Failed to update DOG: ${res.statusText}`);
return res.json();
}
export async function deleteDog(directiveId: string, dogId: string): Promise<void> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}`, {
method: "DELETE",
});
if (!res.ok) throw new Error(`Failed to delete DOG: ${res.statusText}`);
}
export async function pickUpDogOrders(directiveId: string, dogId: string): Promise<PickUpOrdersResponse> {
const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/dogs/${dogId}/pick-up-orders`, {
method: "POST",
});
if (!res.ok) throw new Error(`Failed to pick up DOG 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;
dogId: 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;
dogId?: string | null;
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;
dogId?: string | null;
repositoryUrl?: string | null;
}
export async function listOrders(
status?: OrderStatus,
type?: OrderType,
priority?: OrderPriority,
directiveId?: string,
search?: string,
dogId?: string,
): Promise<OrderListResponse> {
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);
if (dogId) params.set("dogId", dogId);
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<Order> {
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<Order> {
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<Order> {
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<void> {
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<Order> {
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<DirectiveStep> {
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();
}