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 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;
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;
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;
}
export interface TaskWithSubtasks extends Task {
subtasks: TaskSummary[];
}
export interface TaskListResponse {
tasks: TaskSummary[];
total: number;
}
export interface CreateTaskRequest {
/** Contract this task belongs to (required) */
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;
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();
}
/** 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();
}
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();
}
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();
}
// =============================================================================
// 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();
}
// =============================================================================
// Contract Types for Workflow Management
// =============================================================================
/** Contract type determines the workflow and required documents */
export type ContractType = "simple" | "specification";
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"];
}
return ["research", "specify", "plan", "execute", "review"];
}
/** Get default initial phase for a contract type */
export function getDefaultPhase(contractType: ContractType): ContractPhase {
return contractType === "simple" ? "plan" : "research";
}
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;
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;
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) or "specification" */
contractType?: ContractType;
/** Initial phase to start in (defaults based on contract type) */
initialPhase?: ContractPhase;
}
export interface UpdateContractRequest {
name?: string;
description?: string;
phase?: ContractPhase;
status?: ContractStatus;
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.
*/
export async function changeContractPhase(
id: string,
phase: ContractPhase
): Promise<ContractSummary> {
const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/phase`, {
method: "POST",
body: JSON.stringify({ phase }),
});
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}`);
}
}
// =============================================================================
// 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();
}