From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 11 Jan 2026 05:52:14 +0000 Subject: Contract system --- makima/frontend/src/lib/api.ts | 542 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 540 insertions(+), 2 deletions(-) (limited to 'makima/frontend/src/lib/api.ts') diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index a11f15e..d77c85c 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -132,7 +132,8 @@ export type BodyElement = data: Record[]; config?: Record; } - | { type: "image"; src: string; alt?: string; caption?: string }; + | { type: "image"; src: string; alt?: string; caption?: string } + | { type: "markdown"; content: string }; export interface FileSummary { id: string; @@ -141,8 +142,16 @@ export interface FileSummary { 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 { @@ -155,6 +164,12 @@ export interface FileDetail { 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; } @@ -165,10 +180,14 @@ export interface FileListResponse { } export interface CreateFileRequest { + /** Contract this file belongs to (required - files must belong to a contract) */ + contractId: string; name?: string; description?: string; - transcript: TranscriptEntry[]; + transcript?: TranscriptEntry[]; location?: string; + /** Initial body elements (e.g., from a template) */ + body?: BodyElement[]; } export interface UpdateFileRequest { @@ -400,6 +419,23 @@ export async function restoreFileVersion( 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 // ============================================================================= @@ -490,6 +526,12 @@ 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; @@ -497,6 +539,8 @@ export interface TaskSummary { priority: number; progressSummary: string | null; subtaskCount: number; + /** Whether this is a supervisor task (contract orchestrator) */ + isSupervisor: boolean; version: number; createdAt: string; updatedAt: string; @@ -505,6 +549,8 @@ export interface TaskSummary { export interface Task { id: string; ownerId: string; + /** Contract this task belongs to */ + contractId: string | null; parentTaskId: string | null; depth: number; name: string; @@ -556,6 +602,8 @@ export interface TaskListResponse { } export interface CreateTaskRequest { + /** Contract this task belongs to (required) */ + contractId: string; name: string; description?: string; plan: string; @@ -1289,3 +1337,493 @@ export async function deleteAccount( } return res.json(); } + +// ============================================================================= +// Contract Types for Workflow Management +// ============================================================================= + +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"; + +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; + 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; + 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 | null; + createdAt: string; +} + +export interface ContractListResponse { + contracts: ContractSummary[]; + total: number; +} + +export interface CreateContractRequest { + name: string; + description?: string; + /** Initial phase to start in (defaults to "research") */ + 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 { + const res = await authFetch(`${API_BASE}/api/v1/contracts`); + if (!res.ok) { + throw new Error(`Failed to list contracts: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get a contract with all its relations. + */ +export async function getContract(id: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`); + if (!res.ok) { + throw new Error(`Failed to get contract: ${res.statusText}`); + } + return res.json(); +} + +/** + * Create a new contract. + */ +export async function createContract( + data: CreateContractRequest +): Promise { + const res = await authFetch(`${API_BASE}/api/v1/contracts`, { + method: "POST", + body: JSON.stringify(data), + }); + if (!res.ok) { + throw new Error(`Failed to create contract: ${res.statusText}`); + } + return res.json(); +} + +/** + * Update a contract. + */ +export async function updateContract( + id: string, + data: UpdateContractRequest +): Promise { + const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }); + + if (res.status === 409) { + const conflict = (await res.json()) as ConflictErrorResponse; + throw new VersionConflictError(conflict); + } + + if (!res.ok) { + throw new Error(`Failed to update contract: ${res.statusText}`); + } + return res.json(); +} + +/** + * Delete a contract. + */ +export async function deleteContract(id: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, { + method: "DELETE", + }); + if (!res.ok) { + throw new Error(`Failed to delete contract: ${res.statusText}`); + } +} + +/** + * Change contract phase. + */ +export async function changeContractPhase( + id: string, + phase: ContractPhase +): Promise { + 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 { + const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/events`); + if (!res.ok) { + throw new Error(`Failed to get events: ${res.statusText}`); + } + return res.json(); +} + +// ============================================================================= +// Contract Repository Management +// ============================================================================= + +/** + * Add a remote repository to a contract. + */ +export async function addRemoteRepository( + contractId: string, + data: AddRemoteRepositoryRequest +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/repositories/remote`, + { + method: "POST", + body: JSON.stringify(data), + } + ); + if (!res.ok) { + throw new Error(`Failed to add remote repository: ${res.statusText}`); + } + return res.json(); +} + +/** + * Add a local repository to a contract. + */ +export async function addLocalRepository( + contractId: string, + data: AddLocalRepositoryRequest +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/repositories/local`, + { + method: "POST", + body: JSON.stringify(data), + } + ); + if (!res.ok) { + throw new Error(`Failed to add local repository: ${res.statusText}`); + } + return res.json(); +} + +/** + * Create a managed repository (daemon will create it). + */ +export async function createManagedRepository( + contractId: string, + data: CreateManagedRepositoryRequest +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/repositories/managed`, + { + method: "POST", + body: JSON.stringify(data), + } + ); + if (!res.ok) { + throw new Error(`Failed to create managed repository: ${res.statusText}`); + } + return res.json(); +} + +/** + * Delete a repository from a contract. + */ +export async function deleteContractRepository( + contractId: string, + repoId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}`, + { + method: "DELETE", + } + ); + if (!res.ok) { + throw new Error(`Failed to delete repository: ${res.statusText}`); + } +} + +/** + * Set a repository as primary. + */ +export async function setRepositoryPrimary( + contractId: string, + repoId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}/primary`, + { + method: "PUT", + } + ); + if (!res.ok) { + throw new Error(`Failed to set repository as primary: ${res.statusText}`); + } +} + +// ============================================================================= +// Contract Task Association +// ============================================================================= + +/** + * Add a task to a contract. + */ +export async function addTaskToContract( + contractId: string, + taskId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`, + { + method: "POST", + } + ); + if (!res.ok) { + throw new Error(`Failed to add task to contract: ${res.statusText}`); + } +} + +/** + * Remove a task from a contract. + */ +export async function removeTaskFromContract( + contractId: string, + taskId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`, + { + method: "DELETE", + } + ); + if (!res.ok) { + throw new Error(`Failed to remove task from contract: ${res.statusText}`); + } +} + +// ============================================================================= +// Contract Chat Types and API +// ============================================================================= + +export interface ContractChatRequest { + message: string; + model?: LlmModel; + history?: ChatMessage[]; +} + +export interface ContractToolCallInfo { + name: string; + result: { + success: boolean; + message: string; + }; +} + +export interface ContractChatResponse { + response: string; + toolCalls: ContractToolCallInfo[]; + pendingQuestions?: UserQuestion[]; +} + +/** + * Chat with a contract using LLM-powered management tools. + */ +export async function chatWithContract( + contractId: string, + message: string, + model?: LlmModel, + history?: ChatMessage[] +): Promise { + const body: ContractChatRequest = { message }; + if (model) { + body.model = model; + } + if (history && history.length > 0) { + body.history = history; + } + const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/chat`, { + method: "POST", + body: JSON.stringify(body), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Contract chat failed: ${errorText || res.statusText}`); + } + return res.json(); +} + +// Contract chat history types +export interface ContractChatMessage { + id: string; + conversationId: string; + role: "user" | "assistant" | "error"; + content: string; + toolCalls?: unknown; + pendingQuestions?: unknown; + createdAt: string; +} + +export interface ContractChatHistoryResponse { + contractId: string; + conversationId: string; + messages: ContractChatMessage[]; +} + +/** Get contract chat history */ +export async function getContractChatHistory( + contractId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/chat/history` + ); + if (!res.ok) { + throw new Error(`Failed to fetch contract chat history: ${res.statusText}`); + } + return res.json(); +} + +/** Clear contract chat history (starts a new conversation) */ +export async function clearContractChatHistory( + contractId: string +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/chat/history`, + { method: "DELETE" } + ); + if (!res.ok) { + throw new Error(`Failed to clear contract chat history: ${res.statusText}`); + } +} + +// ============================================================================= +// Template Types and API +// ============================================================================= + +export interface TemplateSummary { + id: string; + name: string; + phase: ContractPhase; + description: string; + elementCount: number; +} + +export interface FileTemplate { + id: string; + name: string; + phase: ContractPhase; + description: string; + suggestedBody: BodyElement[]; +} + +export interface ListTemplatesResponse { + templates: TemplateSummary[]; +} + +export async function listTemplates( + phase?: ContractPhase +): Promise { + const params = phase ? `?phase=${phase}` : ""; + const res = await authFetch(`${API_BASE}/api/v1/templates${params}`); + if (!res.ok) { + throw new Error(`Failed to list templates: ${res.statusText}`); + } + return res.json(); +} + +export async function getTemplate(id: string): Promise { + const res = await authFetch(`${API_BASE}/api/v1/templates/${id}`); + if (!res.ok) { + throw new Error(`Failed to get template: ${res.statusText}`); + } + return res.json(); +} -- cgit v1.2.3