summaryrefslogtreecommitdiff
path: root/makima/frontend/src/lib/api.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/lib/api.ts
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/lib/api.ts')
-rw-r--r--makima/frontend/src/lib/api.ts542
1 files changed, 540 insertions, 2 deletions
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<string, unknown>[];
config?: Record<string, unknown>;
}
- | { 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<string, unknown> | 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<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();
+}