import type { Task, TaskDetail, TaskAttachment, TaskComment, TaskCreateInput, AgentLogEntry, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, BatchStatusResult, BatchStatusResponse, BatchStatusEntry, ActivityLogEntry, ActivityEventType, WorkflowStep, WorkflowStepInput, WorkflowStepResult, } from "@fusion/core"; import type { PlanningQuestion, PlanningSummary, PlanningResponse } from "@fusion/core"; import type { ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, AutomationStep } from "@fusion/core"; function looksLikeHtml(body: string): boolean { const trimmed = body.trim(); return trimmed.startsWith("(path: string, opts: RequestInit = {}): Promise { const url = buildApiUrl(path); const res = await fetch(url, { headers: { "Content-Type": "application/json" }, ...opts, }); const contentType = res.headers.get("content-type") ?? ""; const bodyText = await res.text(); const isJson = contentType.includes("application/json"); const isHtml = contentType.includes("text/html") || looksLikeHtml(bodyText); if (isHtml) { throw new Error( `API returned HTML instead of JSON for ${url}. ` + `The endpoint may not be properly configured. (${res.status} ${res.statusText})` ); } if (!isJson) { const preview = bodyText.length > 160 ? `${bodyText.slice(0, 160)}...` : bodyText; throw new Error( `API returned ${contentType || "an unknown content type"} instead of JSON for ${url}. ` + `(${res.status} ${res.statusText})${preview ? ` Response: ${preview}` : ""}` ); } let data: unknown; try { data = bodyText ? JSON.parse(bodyText) : null; } catch { throw new Error( `API returned invalid JSON for ${url}. (${res.status} ${res.statusText})` ); } if (!res.ok) { throw new Error((data as { error?: string } | null)?.error || `Request failed for ${url}: ${res.status} ${res.statusText}`); } return data as T; } export function fetchTasks(limit?: number, offset?: number, projectId?: string): Promise { const search = new URLSearchParams(); if (limit !== undefined) search.set("limit", String(limit)); if (offset !== undefined) search.set("offset", String(offset)); if (projectId) search.set("projectId", projectId); const suffix = search.size > 0 ? `?${search.toString()}` : ""; return api(`/tasks${suffix}`); } export async function fetchTaskDetail(id: string, projectId?: string): Promise { const maxAttempts = 2; // 1 initial + 1 retry const url = buildApiUrl(withProjectId(`/tasks/${id}`, projectId)); for (let attempt = 1; attempt <= maxAttempts; attempt++) { const res = await fetch(url, { headers: { "Content-Type": "application/json" }, }); const data = await res.json(); if (res.ok) return data as TaskDetail; if (attempt === maxAttempts) { throw new Error((data as { error?: string }).error || "Request failed"); } } // unreachable throw new Error("Request failed"); } export function createTask(input: TaskCreateInput, projectId?: string): Promise { const { title, description, column, dependencies, breakIntoSubtasks, enabledWorkflowSteps, modelPresetId, modelProvider, modelId, validatorModelProvider, validatorModelId, } = input; return api(withProjectId("/tasks", projectId), { method: "POST", body: JSON.stringify({ title, description, column, dependencies, breakIntoSubtasks, enabledWorkflowSteps, modelPresetId, modelProvider, modelId, validatorModelProvider, validatorModelId, }), }); } export function updateTask(id: string, updates: { title?: string; description?: string; prompt?: string; dependencies?: string[]; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null }, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** * Batch update AI model configuration for multiple tasks. * @param taskIds - Array of task IDs to update * @param modelProvider - Executor model provider (optional, null to clear) * @param modelId - Executor model ID (optional, null to clear) * @param validatorModelProvider - Validator model provider (optional, null to clear) * @param validatorModelId - Validator model ID (optional, null to clear) * @returns Promise with updated tasks and count */ export function batchUpdateTaskModels( taskIds: string[], modelProvider?: string | null, modelId?: string | null, validatorModelProvider?: string | null, validatorModelId?: string | null, projectId?: string, ): Promise<{ updated: Task[]; count: number }> { return api<{ updated: Task[]; count: number }>(withProjectId("/tasks/batch-update-models", projectId), { method: "POST", body: JSON.stringify({ taskIds, modelProvider, modelId, validatorModelProvider, validatorModelId, }), }); } export function moveTask(id: string, column: Column, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/move`, projectId), { method: "POST", body: JSON.stringify({ column }), }); } export function deleteTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}`, projectId), { method: "DELETE" }); } export function mergeTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/merge`, projectId), { method: "POST" }); } export function retryTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/retry`, projectId), { method: "POST" }); } export function duplicateTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/duplicate`, projectId), { method: "POST" }); } export function pauseTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/pause`, projectId), { method: "POST" }); } export function unpauseTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/unpause`, projectId), { method: "POST" }); } export function archiveTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/archive`, projectId), { method: "POST" }); } export function unarchiveTask(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/unarchive`, projectId), { method: "POST" }); } export function archiveAllDone(projectId?: string): Promise { return api<{ archived: Task[] }>(withProjectId("/tasks/archive-all-done", projectId), { method: "POST" }).then( (response) => response.archived ); } export function approvePlan(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/approve-plan`, projectId), { method: "POST" }); } export function rejectPlan(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/reject-plan`, projectId), { method: "POST" }); } export function fetchConfig(projectId?: string): Promise<{ maxConcurrent: number; rootDir: string }> { return api<{ maxConcurrent: number; rootDir: string }>(withProjectId("/config", projectId)); } export function fetchSettings(projectId?: string): Promise { return api(withProjectId("/settings", projectId)); } export function updateSettings(settings: Partial, projectId?: string): Promise { return api(withProjectId("/settings", projectId), { method: "PUT", body: JSON.stringify(settings), }); } /** Fetch global (user-level) settings from ~/.pi/fusion/settings.json */ export function fetchGlobalSettings(): Promise { return api("/settings/global"); } /** Update global (user-level) settings. These persist across all kb projects. */ export function updateGlobalSettings(settings: Partial): Promise { return api("/settings/global", { method: "PUT", body: JSON.stringify(settings), }); } /** Fetch settings separated by scope: { global, project } */ export function fetchSettingsByScope(projectId?: string): Promise<{ global: GlobalSettings; project: Partial }> { return api<{ global: GlobalSettings; project: Partial }>(withProjectId("/settings/scopes", projectId)); } export function testNtfyNotification(config?: { ntfyEnabled?: boolean; ntfyTopic?: string }, projectId?: string): Promise<{ success: boolean }> { return api<{ success: boolean }>(withProjectId("/settings/test-ntfy", projectId), { method: "POST", body: config ? JSON.stringify(config) : undefined, }); } export async function uploadAttachment(id: string, file: File, projectId?: string): Promise { const formData = new FormData(); formData.append("file", file); const res = await fetch(buildApiUrl(withProjectId(`/tasks/${id}/attachments`, projectId)), { method: "POST", body: formData, }); const data = await res.json(); if (!res.ok) throw new Error((data as { error?: string }).error || "Upload failed"); return data as TaskAttachment; } export async function deleteAttachment(id: string, filename: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/attachments/${filename}`, projectId), { method: "DELETE" }); } export function fetchAgentLogs(taskId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${taskId}/logs`, projectId)); } export function fetchSessionFiles(taskId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${taskId}/session-files`, projectId)); } export function fetchTaskComments(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/comments`, projectId)); } export function addTaskComment(id: string, text: string, author?: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/comments`, projectId), { method: "POST", body: JSON.stringify({ text, author }), }); } export function updateTaskComment(id: string, commentId: string, text: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), { method: "PATCH", body: JSON.stringify({ text }), }); } export function deleteTaskComment(id: string, commentId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), { method: "DELETE", }); } export function addSteeringComment(id: string, text: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/steer`, projectId), { method: "POST", body: JSON.stringify({ text }), }); } export function requestSpecRevision(id: string, feedback: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/spec/revise`, projectId), { method: "POST", body: JSON.stringify({ feedback }), }); } export function refineTask(id: string, feedback: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/refine`, projectId), { method: "POST", body: JSON.stringify({ feedback }), }); } // --- Models API --- /** Available AI model info returned by the models endpoint */ export interface ModelInfo { provider: string; id: string; name: string; reasoning: boolean; contextWindow: number; } /** Response from the models endpoint */ export interface ModelsResponse { models: ModelInfo[]; favoriteProviders: string[]; favoriteModels: string[]; } /** Fetch available AI models from the model registry along with favoriteProviders */ export function fetchModels(): Promise { return api("/models"); } // --- Usage API --- /** Pace information for weekly usage windows */ export interface UsagePace { status: "ahead" | "on-track" | "behind"; percentElapsed: number; // 0-100, how much of the window time has passed message: string; // e.g., "Using 15% over your limit pace" } /** Usage window for a provider (e.g., "Session (5h)", "Weekly") */ export interface UsageWindow { label: string; percentUsed: number; // 0-100 percentLeft: number; // 0-100 resetText: string | null; // e.g., "resets in 2h" resetMs?: number; // ms until reset resetAt?: string; // ISO 8601 timestamp of when the window resets (machine-readable) windowDurationMs?: number; // total window length pace?: UsagePace; // pace indicator for weekly windows } /** Provider usage data */ export interface ProviderUsage { name: string; icon: string; // emoji status: "ok" | "error" | "no-auth"; error?: string; plan?: string | null; email?: string | null; windows: UsageWindow[]; } /** Fetch usage data from all configured AI providers */ export function fetchUsageData(): Promise<{ providers: ProviderUsage[] }> { return api<{ providers: ProviderUsage[] }>("/usage"); } // --- Auth API --- /** OAuth provider with current authentication status */ export interface AuthProvider { id: string; name: string; authenticated: boolean; } /** Fetch authentication status for all OAuth providers */ export function fetchAuthStatus(): Promise<{ providers: AuthProvider[] }> { return api<{ providers: AuthProvider[] }>("/auth/status"); } /** Initiate OAuth login for a provider. Returns the auth URL to open in a new tab. */ export function loginProvider(provider: string): Promise<{ url: string; instructions?: string }> { return api<{ url: string; instructions?: string }>("/auth/login", { method: "POST", body: JSON.stringify({ provider }), }); } /** Logout from a provider, removing stored credentials. */ export function logoutProvider(provider: string): Promise<{ success: boolean }> { return api<{ success: boolean }>("/auth/logout", { method: "POST", body: JSON.stringify({ provider }), }); } // --- GitHub Import API --- /** GitHub issue returned by the fetch endpoint */ export interface GitHubIssue { number: number; title: string; body: string | null; html_url: string; labels: Array<{ name: string }>; } /** Fetch open GitHub issues from a repository */ export function apiFetchGitHubIssues( owner: string, repo: string, limit?: number, labels?: string[] ): Promise { return api("/github/issues/fetch", { method: "POST", body: JSON.stringify({ owner, repo, limit, labels }), }); } /** Import a specific GitHub issue as a kb task */ export function apiImportGitHubIssue(owner: string, repo: string, issueNumber: number): Promise { return api("/github/issues/import", { method: "POST", body: JSON.stringify({ owner, repo, issueNumber }), }); } /** Result of a batch import operation for a single issue */ export interface BatchImportResult { issueNumber: number; success: boolean; taskId?: string; error?: string; skipped?: boolean; retryAfter?: number; } /** Batch import multiple GitHub issues as kb tasks with throttling */ export function apiBatchImportGitHubIssues( owner: string, repo: string, issueNumbers: number[], delayMs?: number ): Promise<{ results: BatchImportResult[] }> { return api<{ results: BatchImportResult[] }>("/github/issues/batch-import", { method: "POST", body: JSON.stringify({ owner, repo, issueNumbers, delayMs }), }); } // --- GitHub Pull Request Import API --- /** GitHub pull request returned by the fetch endpoint */ export interface GitHubPull { number: number; title: string; body: string | null; html_url: string; headBranch: string; baseBranch: string; } /** Fetch open GitHub pull requests from a repository */ export function apiFetchGitHubPulls( owner: string, repo: string, limit?: number ): Promise { return api("/github/pulls/fetch", { method: "POST", body: JSON.stringify({ owner, repo, limit }), }); } /** Import a specific GitHub pull request as a kb review task */ export function apiImportGitHubPull(owner: string, repo: string, prNumber: number): Promise { return api("/github/pulls/import", { method: "POST", body: JSON.stringify({ owner, repo, prNumber }), }); } // --- Git Remote Detection API --- /** Git remote info returned by the remotes endpoint */ export interface GitRemote { name: string; owner: string; repo: string; url: string; } /** Fetch GitHub remotes from the current git repository */ export function fetchGitRemotes(): Promise { return api("/git/remotes"); } /** Detailed git remote info with fetch and push URLs */ export interface GitRemoteDetailed { name: string; fetchUrl: string; pushUrl: string; } /** Fetch all git remotes with their fetch and push URLs */ export function fetchGitRemotesDetailed(): Promise { return api("/git/remotes/detailed"); } /** Add a new git remote */ export function addGitRemote(name: string, url: string): Promise { return api("/git/remotes", { method: "POST", body: JSON.stringify({ name, url }), }); } /** Remove a git remote */ export function removeGitRemote(name: string): Promise { return api(`/git/remotes/${encodeURIComponent(name)}`, { method: "DELETE", }); } /** Rename a git remote */ export function renameGitRemote(name: string, newName: string): Promise { return api(`/git/remotes/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify({ newName }), }); } /** Update the URL for a git remote */ export function updateGitRemoteUrl(name: string, url: string): Promise { return api(`/git/remotes/${encodeURIComponent(name)}/url`, { method: "PUT", body: JSON.stringify({ url }), }); } // --- PR Management API --- /** PR info returned by PR endpoints */ export interface PrInfo { url: string; number: number; status: "open" | "closed" | "merged"; title: string; headBranch: string; baseBranch: string; commentCount: number; lastCommentAt?: string; lastCheckedAt?: string; } export interface PrCheckStatus { name: string; required: boolean; state: string; } export interface PrStatusResponse { prInfo: PrInfo; stale: boolean; automationStatus?: string | null; } export interface PrRefreshResponse { prInfo: PrInfo; mergeReady: boolean; blockingReasons: string[]; reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null; checks: PrCheckStatus[]; automationStatus?: string | null; } /** Create a GitHub PR for a task */ export function createPr( id: string, params: { title: string; body?: string; base?: string }, projectId?: string, ): Promise { return api(withProjectId(`/tasks/${id}/pr/create`, projectId), { method: "POST", body: JSON.stringify(params), }); } /** Fetch cached PR status for a task */ export function fetchPrStatus(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/pr/status`, projectId)); } /** Force refresh PR status from GitHub */ export function refreshPrStatus(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/pr/refresh`, projectId), { method: "POST", }); } // --- Issue Management API --- /** Re-export GitHub badge-related types for convenience */ export type { IssueInfo, BatchStatusResult, BatchStatusEntry } from "@fusion/core"; /** Fetch cached issue status for a task */ export function fetchIssueStatus(id: string, projectId?: string): Promise<{ issueInfo: import("@fusion/core").IssueInfo; stale: boolean }> { return api<{ issueInfo: import("@fusion/core").IssueInfo; stale: boolean }>(withProjectId(`/tasks/${id}/issue/status`, projectId)); } /** Force refresh issue status from GitHub */ export function refreshIssueStatus(id: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${id}/issue/refresh`, projectId), { method: "POST", }); } /** Batch-refresh cached GitHub badge status for multiple tasks. */ export async function fetchBatchStatus(taskIds: string[], projectId?: string): Promise { const response = await api(withProjectId("/github/batch/status", projectId), { method: "POST", body: JSON.stringify({ taskIds }), }); return response.results; } // --- Terminal API --- /** Terminal exec response - returns sessionId for streaming output via SSE */ export interface TerminalExecResponse { sessionId: string; } /** Terminal session status and output */ export interface TerminalSession { id: string; command: string; running: boolean; exitCode: number | null; output: string; startTime: string; } /** Terminal SSE event types */ export interface TerminalOutputEvent { type: "stdout" | "stderr"; data: string; } /** Terminal exit event from SSE */ export interface TerminalExitEvent { type: "exit"; exitCode: number; } /** Execute a shell command and get a session ID for streaming output */ export function execTerminalCommand(command: string): Promise { return api("/terminal/exec", { method: "POST", body: JSON.stringify({ command }), }); } /** Get terminal session status and accumulated output */ export function getTerminalSession(sessionId: string): Promise { return api(`/terminal/sessions/${encodeURIComponent(sessionId)}`); } /** Kill a running terminal session */ export function killTerminalSession(sessionId: string, signal?: "SIGTERM" | "SIGKILL" | "SIGINT"): Promise<{ killed: boolean; sessionId: string }> { return api<{ killed: boolean; sessionId: string }>(`/terminal/sessions/${encodeURIComponent(sessionId)}/kill`, { method: "POST", body: JSON.stringify({ signal: signal ?? "SIGTERM" }), }); } /** Get the SSE stream URL for a terminal session */ export function getTerminalStreamUrl(sessionId: string): string { return `/api/terminal/sessions/${encodeURIComponent(sessionId)}/stream`; } // --- PTY Terminal API (WebSocket-based) --- /** PTY Terminal session response */ export interface PtyTerminalSession { sessionId: string; shell: string; cwd: string; } /** PTY Terminal session info for listing */ export interface PtyTerminalSessionInfo { id: string; cwd: string; shell: string; createdAt: string; } /** Create a new PTY terminal session */ export function createTerminalSession( cwd?: string, cols?: number, rows?: number ): Promise { return api("/terminal/sessions", { method: "POST", body: JSON.stringify({ cwd, cols, rows }), }); } /** Kill a PTY terminal session */ export function killPtyTerminalSession(sessionId: string): Promise<{ killed: boolean }> { return api<{ killed: boolean }>(`/terminal/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE", }); } /** List active PTY terminal sessions */ export function listTerminalSessions(): Promise { return api("/terminal/sessions"); } // --- Git Management API --- /** Current git status */ export interface GitStatus { branch: string; commit: string; isDirty: boolean; ahead: number; behind: number; } /** Git commit info */ export interface GitCommit { hash: string; shortHash: string; message: string; author: string; date: string; parents: string[]; } /** Git branch info */ export interface GitBranch { name: string; isCurrent: boolean; remote?: string; lastCommitDate?: string; } /** Git worktree info */ export interface GitWorktree { path: string; branch?: string; isMain: boolean; isBare: boolean; taskId?: string; } /** Result of a fetch operation */ export interface GitFetchResult { fetched: boolean; message: string; } /** Result of a pull operation */ export interface GitPullResult { success: boolean; message: string; conflict?: boolean; } /** Result of a push operation */ export interface GitPushResult { success: boolean; message: string; } /** Fetch current git status */ export function fetchGitStatus(): Promise { return api("/git/status"); } /** Fetch recent commits */ export function fetchGitCommits(limit?: number): Promise { const query = limit ? `?limit=${limit}` : ""; return api(`/git/commits${query}`); } /** Fetch diff for a specific commit */ export function fetchCommitDiff(hash: string): Promise<{ stat: string; patch: string }> { return api<{ stat: string; patch: string }>(`/git/commits/${hash}/diff`); } /** Fetch all local branches */ export function fetchGitBranches(): Promise { return api("/git/branches"); } /** Fetch all worktrees */ export function fetchGitWorktrees(): Promise { return api("/git/worktrees"); } /** Create a new branch */ export function createBranch(name: string, base?: string): Promise { return api("/git/branches", { method: "POST", body: JSON.stringify({ name, base }), }); } /** Checkout an existing branch */ export function checkoutBranch(name: string): Promise { return api(`/git/branches/${encodeURIComponent(name)}/checkout`, { method: "POST", }); } /** Delete a branch */ export function deleteBranch(name: string, force?: boolean): Promise { const query = force ? "?force=true" : ""; return api(`/git/branches/${encodeURIComponent(name)}${query}`, { method: "DELETE", }); } /** Fetch from remote */ export function fetchRemote(remote?: string): Promise { return api("/git/fetch", { method: "POST", body: JSON.stringify({ remote }), }); } /** Pull current branch */ export function pullBranch(): Promise { return api("/git/pull", { method: "POST", }); } /** Push current branch */ export function pushBranch(): Promise { return api("/git/push", { method: "POST", }); } /** Git stash entry */ export interface GitStash { index: number; message: string; date: string; branch: string; } /** Individual file change with staging status */ export interface GitFileChange { file: string; status: "added" | "modified" | "deleted" | "renamed" | "copied" | "untracked"; staged: boolean; oldFile?: string; } /** Fetch stash list */ export function fetchGitStashList(): Promise { return api("/git/stashes"); } /** Create a new stash */ export function createStash(message?: string): Promise<{ message: string }> { return api<{ message: string }>("/git/stashes", { method: "POST", body: JSON.stringify({ message }), }); } /** Apply a stash entry */ export function applyStash(index: number, drop?: boolean): Promise<{ message: string }> { return api<{ message: string }>(`/git/stashes/${index}/apply`, { method: "POST", body: JSON.stringify({ drop }), }); } /** Drop a stash entry */ export function dropStash(index: number): Promise<{ message: string }> { return api<{ message: string }>(`/git/stashes/${index}`, { method: "DELETE", }); } /** Fetch unstaged diff (working directory changes) */ export function fetchUnstagedDiff(): Promise<{ stat: string; patch: string }> { return api<{ stat: string; patch: string }>("/git/diff"); } /** Fetch file changes (staged and unstaged) */ export function fetchFileChanges(): Promise { return api("/git/changes"); } /** Stage specific files */ export function stageFiles(files: string[]): Promise<{ staged: string[] }> { return api<{ staged: string[] }>("/git/stage", { method: "POST", body: JSON.stringify({ files }), }); } /** Unstage specific files */ export function unstageFiles(files: string[]): Promise<{ unstaged: string[] }> { return api<{ unstaged: string[] }>("/git/unstage", { method: "POST", body: JSON.stringify({ files }), }); } /** Create a commit */ export function createCommit(message: string): Promise<{ hash: string; message: string }> { return api<{ hash: string; message: string }>("/git/commit", { method: "POST", body: JSON.stringify({ message }), }); } /** Discard changes in working directory for specific files */ export function discardChanges(files: string[]): Promise<{ discarded: string[] }> { return api<{ discarded: string[] }>("/git/discard", { method: "POST", body: JSON.stringify({ files }), }); } // --- File Browser API --- /** File node in directory listing */ export interface FileNode { name: string; type: "file" | "directory"; size?: number; mtime?: string; } /** File listing response */ export interface FileListResponse { path: string; entries: FileNode[]; } /** File content response */ export interface FileContentResponse { content: string; mtime: string; size: number; } /** Save file response */ export interface SaveFileResponse { success: true; mtime: string; size: number; } /** List files in task directory */ export function fetchFileList(taskId: string, path?: string): Promise { const query = path ? `?path=${encodeURIComponent(path)}` : ""; return api(`/tasks/${taskId}/files${query}`); } /** Fetch file content */ export function fetchFileContent(taskId: string, filePath: string): Promise { return api(`/tasks/${taskId}/files/${encodeURIComponent(filePath)}`); } /** Save file content */ export function saveFileContent(taskId: string, filePath: string, content: string): Promise { return api(`/tasks/${taskId}/files/${encodeURIComponent(filePath)}`, { method: "POST", body: JSON.stringify({ content }), }); } // --- Workspace File Browser API --- export interface WorkspaceTaskInfo { id: string; title?: string; worktree: string; } export interface WorkspaceListResponse { project: string; tasks: WorkspaceTaskInfo[]; } /** Fetch available file browser workspaces. */ export function fetchWorkspaces(): Promise { return api("/workspaces"); } /** List files in a workspace (project root or task worktree). */ export function fetchWorkspaceFileList(workspace: string, path?: string): Promise { const query = new URLSearchParams({ workspace }); if (path) { query.set("path", path); } return api(`/files?${query.toString()}`); } /** Fetch file content from a workspace. */ export function fetchWorkspaceFileContent(workspace: string, filePath: string): Promise { const query = new URLSearchParams({ workspace }); return api(`/files/${encodeURIComponent(filePath)}?${query.toString()}`); } /** Save file content to a workspace. */ export function saveWorkspaceFileContent(workspace: string, filePath: string, content: string): Promise { const query = new URLSearchParams({ workspace }); return api(`/files/${encodeURIComponent(filePath)}?${query.toString()}`, { method: "POST", body: JSON.stringify({ content }), }); } // --- Planning Mode API --- /** Planning session state returned from API */ export interface PlanningSession { sessionId: string; currentQuestion: PlanningQuestion | null; summary: PlanningSummary | null; } export interface SubtaskItem { id: string; title: string; description: string; suggestedSize: "S" | "M" | "L"; dependsOn: string[]; } /** SSE event types for planning session streaming */ export type PlanningStreamEvent = | { type: "thinking"; data: string } | { type: "question"; data: PlanningQuestion } | { type: "summary"; data: PlanningSummary } | { type: "error"; data: string } | { type: "complete"; data: Record }; /** Start a new planning session with an initial plan */ export function startPlanning(initialPlan: string, projectId?: string): Promise { return api(withProjectId("/planning/start", projectId), { method: "POST", body: JSON.stringify({ initialPlan }), }); } /** Start a new planning session with AI streaming support */ export function startPlanningStreaming(initialPlan: string, projectId?: string): Promise<{ sessionId: string }> { return api<{ sessionId: string }>(withProjectId("/planning/start-streaming", projectId), { method: "POST", body: JSON.stringify({ initialPlan }), }); } /** Submit a response to the current planning question */ export function respondToPlanning( sessionId: string, responses: Record, projectId?: string ): Promise { return api(withProjectId("/planning/respond", projectId), { method: "POST", body: JSON.stringify({ sessionId, responses }), }); } /** Cancel an active planning session */ export function cancelPlanning(sessionId: string, projectId?: string): Promise { return api(withProjectId("/planning/cancel", projectId), { method: "POST", body: JSON.stringify({ sessionId }), }); } /** Create a task from a completed planning session */ export function createTaskFromPlanning(sessionId: string, projectId?: string): Promise { return api(withProjectId("/planning/create-task", projectId), { method: "POST", body: JSON.stringify({ sessionId }), }); } /** Get the SSE stream URL for a planning session */ export function getPlanningStreamUrl(sessionId: string, projectId?: string): string { return buildApiUrl(withProjectId(`/planning/${encodeURIComponent(sessionId)}/stream`, projectId)); } /** Connect to planning session SSE stream and handle events * * Returns an object with: * - close: function to close the connection * - reconnect: function to reconnect after error */ export function connectPlanningStream( sessionId: string, projectId: string | undefined, handlers: { onThinking?: (data: string) => void; onQuestion?: (data: PlanningQuestion) => void; onSummary?: (data: PlanningSummary) => void; onError?: (data: string) => void; onComplete?: () => void; } ): { close: () => void; isConnected: () => boolean } { const url = getPlanningStreamUrl(sessionId, projectId); const eventSource = new EventSource(url); let isClosed = false; eventSource.onopen = () => { isClosed = false; }; eventSource.onmessage = (event) => { // Handle comment events (heartbeats) if (event.data.startsWith(":")) return; }; // Handle specific event types eventSource.addEventListener("thinking", (event: Event) => { try { const messageEvent = event as MessageEvent; const data = JSON.parse(messageEvent.data); handlers.onThinking?.(data); } catch { const messageEvent = event as MessageEvent; handlers.onThinking?.(messageEvent.data); } }); eventSource.addEventListener("question", (event: Event) => { try { const messageEvent = event as MessageEvent; const data = JSON.parse(messageEvent.data) as PlanningQuestion; handlers.onQuestion?.(data); } catch (err) { console.error("[planning] Failed to parse question event:", err); } }); eventSource.addEventListener("summary", (event: Event) => { try { const messageEvent = event as MessageEvent; const data = JSON.parse(messageEvent.data) as PlanningSummary; handlers.onSummary?.(data); } catch (err) { console.error("[planning] Failed to parse summary event:", err); } }); eventSource.addEventListener("error", (event: Event) => { try { const messageEvent = event as MessageEvent; const data = JSON.parse(messageEvent.data); handlers.onError?.(data.message || data); } catch { const messageEvent = event as MessageEvent; handlers.onError?.(messageEvent.data || "Stream error"); } close(); }); eventSource.addEventListener("complete", () => { handlers.onComplete?.(); close(); }); // Handle connection errors eventSource.onerror = () => { if (!isClosed) { handlers.onError?.("Connection lost"); close(); } }; function close() { if (!isClosed) { isClosed = true; eventSource.close(); } } return { close, isConnected: () => !isClosed && eventSource.readyState === EventSource.OPEN, }; } // ── Automation / Scheduled Tasks ────────────────────────────────── /** Response from the manual run trigger endpoint. */ export interface AutomationRunResponse { schedule: ScheduledTask; result: AutomationRunResult; } export function fetchAutomations(): Promise { return api("/automations"); } export function fetchAutomation(id: string): Promise { return api(`/automations/${id}`); } export function createAutomation(input: ScheduledTaskCreateInput): Promise { const { name, description, scheduleType, cronExpression, command, enabled, timeoutMs, steps } = input; return api("/automations", { method: "POST", body: JSON.stringify({ name, description, scheduleType, cronExpression, command, enabled, timeoutMs, steps }), }); } export function updateAutomation(id: string, updates: ScheduledTaskUpdateInput): Promise { const { name, description, scheduleType, cronExpression, command, enabled, timeoutMs, steps } = updates; return api(`/automations/${id}`, { method: "PATCH", body: JSON.stringify({ name, description, scheduleType, cronExpression, command, enabled, timeoutMs, steps }), }); } export async function deleteAutomation(id: string): Promise { await api(`/automations/${id}`, { method: "DELETE", }); } export function runAutomation(id: string): Promise { return api(`/automations/${id}/run`, { method: "POST", }); } export function toggleAutomation(id: string): Promise { return api(`/automations/${id}/toggle`, { method: "POST", }); } export function reorderAutomationSteps(id: string, stepIds: string[]): Promise { return api(`/automations/${id}/steps/reorder`, { method: "POST", body: JSON.stringify({ stepIds }), }); } // ── Activity Log API ──────────────────────────────────────────── /** Re-export ActivityLogEntry type from core for convenience */ export type { ActivityLogEntry, ActivityEventType } from "@fusion/core"; /** Fetch activity log entries */ export function fetchActivityLog(options?: { limit?: number; since?: string; type?: ActivityEventType }): Promise { const search = new URLSearchParams(); if (options?.limit !== undefined) search.set("limit", String(options.limit)); if (options?.since !== undefined) search.set("since", options.since); if (options?.type !== undefined) search.set("type", options.type); const suffix = search.size > 0 ? `?${search.toString()}` : ""; return api(`/activity${suffix}`); } /** Clear all activity log entries */ export function clearActivityLog(): Promise<{ success: boolean }> { return api<{ success: boolean }>("/activity", { method: "DELETE" }); } // ── Workflow Steps ───────────────────────────────────────────────────── /** Fetch all workflow step definitions */ export function fetchWorkflowSteps(projectId?: string): Promise { return api(withProjectId("/workflow-steps", projectId)); } /** Create a new workflow step */ export function createWorkflowStep(input: WorkflowStepInput, projectId?: string): Promise { return api(withProjectId("/workflow-steps", projectId), { method: "POST", body: JSON.stringify(input), }); } /** Update a workflow step */ export function updateWorkflowStep(id: string, updates: Partial, projectId?: string): Promise { return api(withProjectId(`/workflow-steps/${id}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Delete a workflow step */ export function deleteWorkflowStep(id: string, projectId?: string): Promise { return api(withProjectId(`/workflow-steps/${id}`, projectId), { method: "DELETE" }); } /** Refine a workflow step's prompt using AI */ export function refineWorkflowStepPrompt(id: string, projectId?: string): Promise<{ prompt: string; workflowStep: WorkflowStep }> { return api<{ prompt: string; workflowStep: WorkflowStep }>(withProjectId(`/workflow-steps/${id}/refine`, projectId), { method: "POST", }); } /** Fetch workflow step results for a task */ export function fetchWorkflowResults(taskId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow-results`, projectId)); } // ── Workflow Step Templates ────────────────────────────────────────────── /** Re-export WorkflowStepTemplate type from core */ export type { WorkflowStepTemplate } from "@fusion/core"; /** Fetch all built-in workflow step templates */ export function fetchWorkflowStepTemplates(): Promise<{ templates: import("@fusion/core").WorkflowStepTemplate[] }> { return api<{ templates: import("@fusion/core").WorkflowStepTemplate[] }>("/workflow-step-templates"); } /** Create a workflow step from a built-in template */ export function createWorkflowStepFromTemplate(templateId: string, projectId?: string): Promise { return api(withProjectId(`/workflow-step-templates/${encodeURIComponent(templateId)}/create`, projectId), { method: "POST", }); } // ── Scripts API ──────────────────────────────────────────────────────── /** Script entry returned from the API */ export interface ScriptEntry { name: string; command: string; } /** Result of running a script via POST /api/scripts/:name/run */ export interface ScriptRunResult { sessionId: string; command: string; } /** Fetch all saved scripts from project settings */ export function fetchScripts(projectId?: string): Promise> { return api>(withProjectId("/scripts", projectId)); } /** Add or update a script */ export function addScript(name: string, command: string, projectId?: string): Promise { return api(withProjectId("/scripts", projectId), { method: "POST", body: JSON.stringify({ name, command }), }); } /** Remove a script by name */ export function removeScript(name: string, projectId?: string): Promise { return api(withProjectId(`/scripts/${encodeURIComponent(name)}`, projectId), { method: "DELETE" }); } /** Run a saved script by name */ export function runScript(name: string, args?: string[], projectId?: string): Promise { return api(withProjectId(`/scripts/${encodeURIComponent(name)}/run`, projectId), { method: "POST", body: JSON.stringify({ args }), }); } // ── AI Text Refinement API ──────────────────────────────────────────── /** Refinement types for AI text refinement */ export type RefinementType = "clarify" | "add-details" | "expand" | "simplify"; /** Response from text refinement endpoint */ export interface RefineTextResponse { refined: string; } /** * Refine task description text using AI. * @param text - The text to refine (1-2000 characters) * @param type - The refinement type: clarify, add-details, expand, or simplify * @returns The refined text * @throws Error with message for rate limit (429), invalid type (422), validation (400), or server errors */ export async function refineText(text: string, type: RefinementType): Promise { const response = await api("/ai/refine-text", { method: "POST", body: JSON.stringify({ text, type }), }); return response.refined; } /** * Error messages for refineText failures (to use with toast notifications). */ export const REFINE_ERROR_MESSAGES = { /** Rate limit exceeded (429) */ RATE_LIMIT: "Too many refinement requests. Please wait an hour.", /** Invalid refinement type (422) */ INVALID_TYPE: "Invalid refinement option selected.", /** Network or server errors */ NETWORK: "Failed to refine text. Please try again.", } as const; /** * Get user-friendly error message for a refineText error. * @param error - The error thrown by refineText * @returns A user-friendly error message suitable for toast display */ export function getRefineErrorMessage(error: unknown): string { if (!(error instanceof Error)) { return REFINE_ERROR_MESSAGES.NETWORK; } const message = error.message.toLowerCase(); // Rate limit errors (429) if (message.includes("rate limit") || message.includes("429")) { return REFINE_ERROR_MESSAGES.RATE_LIMIT; } // Invalid type errors (422) if (message.includes("invalid") && message.includes("type")) { return REFINE_ERROR_MESSAGES.INVALID_TYPE; } // Text validation errors (400) - pass through from backend if ( message.startsWith("text must") || message.includes("text is required") || message.includes("type is required") ) { return error.message; } // Default network/server error return REFINE_ERROR_MESSAGES.NETWORK; } export function startSubtaskBreakdown(description: string, projectId?: string): Promise<{ sessionId: string }> { return api<{ sessionId: string }>(withProjectId("/subtasks/start-streaming", projectId), { method: "POST", body: JSON.stringify({ description }), }); } export function getSubtaskStreamUrl(sessionId: string, projectId?: string): string { return buildApiUrl(withProjectId(`/subtasks/${encodeURIComponent(sessionId)}/stream`, projectId)); } export function connectSubtaskStream( sessionId: string, projectId: string | undefined, handlers: { onThinking?: (data: string) => void; onSubtasks?: (data: SubtaskItem[]) => void; onError?: (data: string) => void; onComplete?: () => void; } ): { close: () => void; isConnected: () => boolean } { const eventSource = new EventSource(getSubtaskStreamUrl(sessionId, projectId)); let isClosed = false; eventSource.onopen = () => { isClosed = false; }; eventSource.addEventListener("thinking", (event: Event) => { const messageEvent = event as MessageEvent; try { handlers.onThinking?.(JSON.parse(messageEvent.data)); } catch { handlers.onThinking?.(messageEvent.data); } }); eventSource.addEventListener("subtasks", (event: Event) => { try { const messageEvent = event as MessageEvent; handlers.onSubtasks?.(JSON.parse(messageEvent.data) as SubtaskItem[]); } catch (err) { console.error("[subtasks] Failed to parse subtasks event:", err); } }); eventSource.addEventListener("error", (event: Event) => { try { const messageEvent = event as MessageEvent; handlers.onError?.(JSON.parse(messageEvent.data) as string); } catch { handlers.onError?.("Stream error"); } isClosed = true; eventSource.close(); }); eventSource.addEventListener("complete", () => { handlers.onComplete?.(); isClosed = true; eventSource.close(); }); eventSource.onerror = () => { if (!isClosed) { handlers.onError?.("Connection lost"); } isClosed = true; eventSource.close(); }; return { close: () => { isClosed = true; eventSource.close(); }, isConnected: () => !isClosed, }; } export function createTasksFromBreakdown( sessionId: string, subtasks: SubtaskItem[], parentTaskId?: string, projectId?: string, ): Promise<{ tasks: Task[]; parentTaskClosed?: boolean }> { return api<{ tasks: Task[]; parentTaskClosed?: boolean }>(withProjectId("/subtasks/create-tasks", projectId), { method: "POST", body: JSON.stringify({ sessionId, parentTaskId, subtasks: subtasks.map((subtask) => ({ tempId: subtask.id, title: subtask.title, description: subtask.description, size: subtask.suggestedSize, dependsOn: subtask.dependsOn, })), }), }); } export function cancelSubtaskBreakdown(sessionId: string, projectId?: string): Promise { return api(withProjectId("/subtasks/cancel", projectId), { method: "POST", body: JSON.stringify({ sessionId }), }); } // ── Agent API ──────────────────────────────────────────────────────────── import type { Agent, AgentDetail, AgentCapability, AgentState, AgentHeartbeatEvent, AgentHeartbeatRun, AgentCreateInput, AgentUpdateInput } from "@fusion/core"; export type { Agent, AgentDetail, AgentCapability, AgentState, AgentHeartbeatEvent, AgentHeartbeatRun, AgentCreateInput, AgentUpdateInput }; function withProjectId(path: string, projectId?: string): string { if (!projectId) return path; const separator = path.includes("?") ? "&" : "?"; return `${path}${separator}projectId=${encodeURIComponent(projectId)}`; } /** Fetch all agents, optionally filtered by state or role */ export function fetchAgents( filter?: { state?: AgentState; role?: AgentCapability }, projectId?: string, ): Promise { const params = new URLSearchParams(); if (filter?.state) params.set("state", filter.state); if (filter?.role) params.set("role", filter.role); if (projectId) params.set("projectId", projectId); const query = params.size > 0 ? `?${params.toString()}` : ""; return api(`/agents${query}`); } /** Fetch a single agent with heartbeat history */ export function fetchAgent(agentId: string, projectId?: string): Promise { return api(withProjectId(`/agents/${encodeURIComponent(agentId)}`, projectId)); } /** Create a new agent */ export function createAgent(input: AgentCreateInput, projectId?: string): Promise { return api(withProjectId("/agents", projectId), { method: "POST", body: JSON.stringify(input), }); } /** Update an agent */ export function updateAgent(agentId: string, updates: AgentUpdateInput, projectId?: string): Promise { return api(withProjectId(`/agents/${encodeURIComponent(agentId)}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Update an agent's state */ export function updateAgentState(agentId: string, state: AgentState, projectId?: string): Promise { return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/state`, projectId), { method: "POST", body: JSON.stringify({ state }), }); } /** Delete an agent */ export function deleteAgent(agentId: string, projectId?: string): Promise { return api(withProjectId(`/agents/${encodeURIComponent(agentId)}`, projectId), { method: "DELETE", }); } /** Record a heartbeat for an agent */ export function recordAgentHeartbeat( agentId: string, status: "ok" | "missed" | "recovered" = "ok", projectId?: string, ): Promise { return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/heartbeat`, projectId), { method: "POST", body: JSON.stringify({ status }), }); } /** Fetch heartbeat history for an agent */ export function fetchAgentHeartbeats(agentId: string, limit?: number, projectId?: string): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (projectId) params.set("projectId", projectId); const query = params.size > 0 ? `?${params.toString()}` : ""; return api(`/agents/${encodeURIComponent(agentId)}/heartbeats${query}`); } // --- Backup API --- /** Backup metadata from the API */ export interface BackupInfo { filename: string; createdAt: string; size: number; path: string; } /** Result of listing backups */ export interface BackupListResponse { backups: BackupInfo[]; count: number; totalSize: number; } /** Result of creating a backup */ export interface BackupCreateResponse { success: boolean; backupPath?: string; output?: string; deletedCount?: number; error?: string; } /** Fetch all database backups */ export function fetchBackups(projectId?: string): Promise { return api(withProjectId("/backups", projectId)); } /** Create a new database backup immediately */ export function createBackup(projectId?: string): Promise { return api(withProjectId("/backups", projectId), { method: "POST" }); } // --- Settings Export/Import API --- /** Exported settings data structure */ export interface SettingsExportData { version: 1; exportedAt: string; source?: string; global?: GlobalSettings; project?: Partial; } /** Result of importing settings */ export interface SettingsImportResponse { success: boolean; globalCount: number; projectCount: number; error?: string; } /** Export settings as JSON */ export function exportSettings(scope?: 'global' | 'project' | 'both', projectId?: string): Promise { const path = withProjectId("/settings/export", projectId); const scopedPath = scope ? `${path}${path.includes("?") ? "&" : "?"}scope=${encodeURIComponent(scope)}` : path; return api(scopedPath); } /** Import settings from JSON data */ export function importSettings( data: SettingsExportData, options?: { scope?: 'global' | 'project' | 'both'; merge?: boolean }, projectId?: string ): Promise { return api(withProjectId("/settings/import", projectId), { method: "POST", body: JSON.stringify({ data, scope: options?.scope ?? "both", merge: options?.merge ?? true, }), }); } // --- AI Summarization API --- /** Response from title summarization endpoint */ export interface SummarizeTitleResponse { title: string; } /** Summarize a task description into a concise title using AI. * @param description - The task description to summarize (must be 141-2000 chars) * @param provider - Optional AI model provider (e.g., "anthropic") * @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5") * @returns The generated title (guaranteed ≤60 characters) * @throws Error with descriptive message for 400/429/503 errors */ export async function summarizeTitle( description: string, provider?: string, modelId?: string ): Promise { const res = await fetch("/api/ai/summarize-title", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ description, provider, modelId }), }); const contentType = res.headers.get("content-type") ?? ""; const bodyText = await res.text(); const isJson = contentType.includes("application/json"); if (!isJson) { throw new Error(`API returned non-JSON response: ${bodyText.slice(0, 100)}`); } const data = JSON.parse(bodyText) as { title?: string; error?: string }; if (!res.ok) { const errorMessage = data.error || "Request failed"; if (res.status === 400) { throw new Error(`Invalid request: ${errorMessage}`); } else if (res.status === 429) { throw new Error(`Rate limit exceeded: ${errorMessage}`); } else if (res.status === 503) { throw new Error(`AI service temporarily unavailable: ${errorMessage}`); } else { throw new Error(errorMessage); } } if (!data.title) { throw new Error("API returned empty title"); } return data.title; } // ── Project Management API (Multi-Project Support) ─────────────────────── /** Project information returned by project endpoints */ export interface ProjectInfo { id: string; name: string; path: string; status: "active" | "paused" | "errored" | "initializing"; isolationMode: "in-process" | "child-process"; createdAt: string; updatedAt: string; lastActivityAt?: string; } /** Project health metrics */ export interface ProjectHealth { projectId: string; status: "active" | "paused" | "errored" | "initializing"; activeTaskCount: number; inFlightAgentCount: number; lastActivityAt?: string; lastErrorAt?: string; lastErrorMessage?: string; totalTasksCompleted: number; totalTasksFailed: number; averageTaskDurationMs?: number; updatedAt: string; } /** Executor state values */ export type ExecutorState = "idle" | "running" | "paused"; /** Aggregated executor statistics for the status bar. * * Counts (runningTaskCount, blockedTaskCount, queuedTaskCount, inReviewCount, stuckTaskCount) * are derived client-side from the same tasks array shared with the board, ensuring * the footer counts always match the column counts displayed on screen. * The API returns settings-based values (globalPause, enginePaused, maxConcurrent) and * lastActivityAt from the activity log. * * The executorState is derived from: * - "idle": globalPause is true OR (enginePaused is true AND runningTaskCount is 0) * - "paused": enginePaused is true AND runningTaskCount > 0 * - "running": globalPause is false AND enginePaused is false AND runningTaskCount > 0 */ export interface ExecutorStats { /** Number of tasks currently in "in-progress" column */ runningTaskCount: number; /** Number of tasks with blockedBy field set (waiting on file overlap) */ blockedTaskCount: number; /** Number of "in-progress" tasks with no activity for > 10 minutes */ stuckTaskCount: number; /** Number of tasks in "todo" column */ queuedTaskCount: number; /** Number of tasks in "in-review" column */ inReviewCount: number; /** Derived executor state: "idle", "running", or "paused" */ executorState: ExecutorState; /** Maximum concurrent tasks allowed from settings */ maxConcurrent: number; /** ISO timestamp of most recent task event from activity log */ lastActivityAt?: string; } /** Unified activity feed entry */ export interface ActivityFeedEntry { id: string; timestamp: string; type: "task:created" | "task:moved" | "task:updated" | "task:deleted" | "task:merged" | "task:failed" | "settings:updated"; projectId: string; projectName: string; taskId?: string; taskTitle?: string; details: string; metadata?: Record; } /** Input for creating a new project */ export interface ProjectCreateInput { name: string; path: string; isolationMode?: "in-process" | "child-process"; } /** Options for fetching activity feed */ export interface FeedOptions { limit?: number; since?: string; projectId?: string; type?: ActivityFeedEntry["type"]; } /** Global concurrency state across all projects */ export interface GlobalConcurrencyState { globalMaxConcurrent: number; currentlyActive: number; queuedCount: number; projectsActive: Record; } /** First run status response */ export interface FirstRunStatus { hasProjects: boolean; singleProjectPath: string | null; } /** Setup state for first-run wizard */ export interface SetupState { /** The first-run state: fresh-install, needs-migration, setup-wizard, normal-operation */ state: "fresh-install" | "needs-migration" | "setup-wizard" | "normal-operation"; /** Projects detected on the filesystem (not yet registered) */ detectedProjects: Array<{ path: string; name: string; hasDb: boolean; }>; /** Whether the central database exists */ hasCentralDb: boolean; /** Projects already registered in the central database */ registeredProjects: Array<{ id: string; name: string; path: string; }>; } /** Input for completing setup */ export interface CompleteSetupInput { projects: Array<{ path: string; name: string; isolationMode?: "in-process" | "child-process"; }>; } /** Result of completing setup */ export interface CompleteSetupResult { success: boolean; projectsRegistered: string[]; errors: string[]; } /** Fetch all registered projects */ export function fetchProjects(): Promise { return api("/projects"); } /** Browse directory entries for the directory picker */ export interface BrowseDirectoryResult { currentPath: string; parentPath: string | null; entries: Array<{ name: string; path: string; hasChildren: boolean }>; } export function browseDirectory(path?: string, showHidden?: boolean): Promise { const params = new URLSearchParams(); if (path) params.set("path", path); if (showHidden) params.set("showHidden", "true"); const qs = params.toString(); return api(`/browse-directory${qs ? `?${qs}` : ""}`); } /** Register a new project */ export function registerProject(input: ProjectCreateInput): Promise { return api("/projects", { method: "POST", body: JSON.stringify(input), }); } /** Unregister a project */ export function unregisterProject(id: string): Promise { return api(`/projects/${encodeURIComponent(id)}`, { method: "DELETE", }); } /** Fetch health metrics for a specific project */ export function fetchProjectHealth(id: string): Promise { return api(`/projects/${encodeURIComponent(id)}/health`); } /** Fetch executor statistics for the status bar. * * Returns settings-based values and lastActivityAt. * Counts are derived client-side from the tasks array. */ export function fetchExecutorStats(projectId?: string): Promise<{ globalPause: boolean; enginePaused: boolean; maxConcurrent: number; lastActivityAt?: string; }> { return api<{ globalPause: boolean; enginePaused: boolean; maxConcurrent: number; lastActivityAt?: string; }>(withProjectId("/executor/stats", projectId)); } /** Fetch unified activity feed */ export function fetchActivityFeed(options?: FeedOptions): Promise { const params = new URLSearchParams(); if (options?.limit !== undefined) params.set("limit", String(options.limit)); if (options?.since) params.set("since", options.since); if (options?.projectId) params.set("projectId", options.projectId); if (options?.type) params.set("type", options.type); const query = params.size > 0 ? `?${params.toString()}` : ""; return api(`/activity-feed${query}`); } /** Pause a project */ export function pauseProject(id: string): Promise { return api(`/projects/${encodeURIComponent(id)}/pause`, { method: "POST", }); } /** Resume a paused project */ export function resumeProject(id: string): Promise { return api(`/projects/${encodeURIComponent(id)}/resume`, { method: "POST", }); } /** Fetch first run status to detect if user needs setup wizard */ export function fetchFirstRunStatus(): Promise { return api("/first-run-status"); } /** Fetch detailed setup state including detected projects */ export function fetchSetupState(): Promise { return api("/setup-state"); } /** Complete first-run setup by registering projects */ export function completeSetup(input: CompleteSetupInput): Promise { return api("/complete-setup", { method: "POST", body: JSON.stringify(input), }); } /** Fetch global concurrency state */ export function fetchGlobalConcurrency(): Promise { return api("/global-concurrency"); } /** Fetch tasks for a specific project */ export function fetchProjectTasks(projectId: string, limit?: number, offset?: number): Promise { const params = new URLSearchParams(); params.set("projectId", projectId); if (limit !== undefined) params.set("limit", String(limit)); if (offset !== undefined) params.set("offset", String(offset)); return api(`/tasks?${params.toString()}`); } /** Fetch project-specific config */ export function fetchProjectConfig(projectId: string): Promise<{ maxConcurrent: number; rootDir: string }> { return api<{ maxConcurrent: number; rootDir: string }>(`/projects/${encodeURIComponent(projectId)}/config`); } /** Detected project information */ export interface DetectedProject { path: string; suggestedName: string; existing: boolean; } /** Detect projects in a base path */ export function detectProjects(basePath?: string): Promise<{ projects: DetectedProject[] }> { return api<{ projects: DetectedProject[] }>("/projects/detect", { method: "POST", body: JSON.stringify({ basePath }), }); } /** Fetch a single project by ID */ export function fetchProject(id: string): Promise { return api(`/projects/${encodeURIComponent(id)}`); } /** Update an existing project */ export function updateProject(id: string, updates: Partial): Promise { return api(`/projects/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(updates), }); } // ── Task Diff API ────────────────────────────────────────────────────────── /** Task diff information */ export interface TaskDiff { files: Array<{ path: string; status: "added" | "modified" | "deleted"; additions: number; deletions: number; patch: string; }>; stats: { filesChanged: number; additions: number; deletions: number; }; } /** Fetch diff for a task's changes */ export function fetchTaskDiff(taskId: string, worktree?: string, projectId?: string): Promise { const params = new URLSearchParams(); if (worktree) params.set("worktree", worktree); if (projectId) params.set("projectId", projectId); const query = params.size > 0 ? `?${params.toString()}` : ""; return api(`/tasks/${encodeURIComponent(taskId)}/diff${query}`); } /** Individual file diff */ export interface TaskFileDiff { path: string; status: "added" | "modified" | "deleted" | "renamed"; diff: string; oldPath?: string; } /** Fetch file diffs for a task */ export function fetchTaskFileDiffs(taskId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${encodeURIComponent(taskId)}/file-diffs`, projectId)); } // ── Mission API ─────────────────────────────────────────────────────────── /** Mission status values */ export type MissionStatus = "planning" | "active" | "blocked" | "complete" | "archived"; /** Milestone status values */ export type MilestoneStatus = "planning" | "active" | "blocked" | "complete"; /** Slice status values */ export type SliceStatus = "pending" | "active" | "complete"; /** Feature status values */ export type FeatureStatus = "defined" | "triaged" | "in-progress" | "done"; /** Mission entity */ export interface Mission { id: string; title: string; description?: string; status: MissionStatus; interviewState: "not_started" | "in_progress" | "completed" | "needs_update"; autoAdvance?: boolean; createdAt: string; updatedAt: string; } /** Milestone entity */ export interface Milestone { id: string; missionId: string; title: string; description?: string; status: MilestoneStatus; orderIndex: number; interviewState: "not_started" | "in_progress" | "completed" | "needs_update"; dependencies: string[]; createdAt: string; updatedAt: string; } /** Slice entity */ export interface Slice { id: string; milestoneId: string; title: string; description?: string; status: SliceStatus; orderIndex: number; activatedAt?: string; createdAt: string; updatedAt: string; } /** Feature entity */ export interface MissionFeature { id: string; sliceId: string; taskId?: string; title: string; description?: string; acceptanceCriteria?: string; status: FeatureStatus; createdAt: string; updatedAt: string; } /** Milestone with slices (each slice has features) */ export interface MilestoneWithSlices extends Milestone { slices: SliceWithFeatures[]; } /** Slice with features */ export interface SliceWithFeatures extends Slice { features: MissionFeature[]; } /** Full mission hierarchy */ export interface MissionWithHierarchy extends Mission { milestones: MilestoneWithSlices[]; } /** Fetch all missions */ export function fetchMissions(projectId?: string): Promise { return api(withProjectId("/missions", projectId)); } /** Create a new mission */ export function createMission(input: { title: string; description?: string }, projectId?: string): Promise { return api(withProjectId("/missions", projectId), { method: "POST", body: JSON.stringify(input), }); } /** Get mission with full hierarchy */ export function fetchMission(missionId: string, projectId?: string): Promise { return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId)); } /** Update mission */ export function updateMission(missionId: string, updates: Partial, projectId?: string): Promise { return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Delete mission */ export function deleteMission(missionId: string, projectId?: string): Promise { return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), { method: "DELETE", }); } /** Get mission computed status */ export function fetchMissionStatus(missionId: string, projectId?: string): Promise<{ status: string }> { return api<{ status: string }>(withProjectId(`/missions/${encodeURIComponent(missionId)}/status`, projectId)); } /** Add milestone to mission */ export function createMilestone( missionId: string, input: { title: string; description?: string; dependencies?: string[] }, projectId?: string ): Promise { return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones`, projectId), { method: "POST", body: JSON.stringify(input), }); } /** Update milestone */ export function updateMilestone(milestoneId: string, updates: Partial, projectId?: string): Promise { return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Delete milestone */ export function deleteMilestone(milestoneId: string, projectId?: string): Promise { return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { method: "DELETE", }); } /** Reorder milestones */ export function reorderMilestones(missionId: string, orderedIds: string[], projectId?: string): Promise { return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones/reorder`, projectId), { method: "POST", body: JSON.stringify({ orderedIds }), }); } /** Add slice to milestone */ export function createSlice( milestoneId: string, input: { title: string; description?: string }, projectId?: string ): Promise { return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices`, projectId), { method: "POST", body: JSON.stringify(input), }); } /** Update slice */ export function updateSlice(sliceId: string, updates: Partial, projectId?: string): Promise { return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Delete slice */ export function deleteSlice(sliceId: string, projectId?: string): Promise { return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { method: "DELETE", }); } /** Activate slice */ export function activateSlice(sliceId: string, projectId?: string): Promise { return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/activate`, projectId), { method: "POST", }); } /** Reorder slices */ export function reorderSlices(milestoneId: string, orderedIds: string[], projectId?: string): Promise { return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices/reorder`, projectId), { method: "POST", body: JSON.stringify({ orderedIds }), }); } /** Add feature to slice */ export function createFeature( sliceId: string, input: { title: string; description?: string; acceptanceCriteria?: string }, projectId?: string ): Promise { return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/features`, projectId), { method: "POST", body: JSON.stringify(input), }); } /** Update feature */ export function updateFeature(featureId: string, updates: Partial, projectId?: string): Promise { return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { method: "PATCH", body: JSON.stringify(updates), }); } /** Delete feature */ export function deleteFeature(featureId: string, projectId?: string): Promise { return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { method: "DELETE", }); } /** Link feature to task */ export function linkFeatureToTask(featureId: string, taskId: string, projectId?: string): Promise { return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/link-task`, projectId), { method: "POST", body: JSON.stringify({ taskId }), }); } /** Unlink feature from task */ export function unlinkFeatureFromTask(featureId: string, projectId?: string): Promise { return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/unlink-task`, projectId), { method: "POST", }); }