import type { Issue, CreateIssueRequest, UpdateIssueRequest, ListIssuesResponse, UpdateMeRequest, CreateMemberRequest, UpdateMemberRequest, ListIssuesParams, Agent, CreateAgentRequest, UpdateAgentRequest, AgentTask, AgentRuntime, DaemonPairingSession, ApproveDaemonPairingSessionRequest, InboxItem, IssueSubscriber, Comment, Workspace, WorkspaceRepo, MemberWithUser, User, Skill, CreateSkillRequest, UpdateSkillRequest, SetAgentSkillsRequest, PersonalAccessToken, CreatePersonalAccessTokenRequest, CreatePersonalAccessTokenResponse, RuntimeUsage, RuntimeHourlyActivity, RuntimePing, TimelineEntry, TaskMessagePayload, } from "@/shared/types"; import { type Logger, noopLogger } from "@/shared/logger"; export interface LoginResponse { token: string; user: User; } export class ApiClient { private baseUrl: string; private token: string | null = null; private workspaceId: string | null = null; private logger: Logger; constructor(baseUrl: string, options?: { logger?: Logger }) { this.baseUrl = baseUrl; this.logger = options?.logger ?? noopLogger; } setToken(token: string | null) { this.token = token; } setWorkspaceId(id: string | null) { this.workspaceId = id; } private async fetch(path: string, init?: RequestInit): Promise { const rid = crypto.randomUUID().slice(0, 8); const start = Date.now(); const method = init?.method ?? "GET"; const headers: Record = { "Content-Type": "application/json", "X-Request-ID": rid, ...((init?.headers as Record) ?? {}), }; if (this.token) { headers["Authorization"] = `Bearer ${this.token}`; } if (this.workspaceId) { headers["X-Workspace-ID"] = this.workspaceId; } this.logger.info(`→ ${method} ${path}`, { rid }); const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, }); if (!res.ok) { if (res.status === 401 && typeof window !== "undefined") { localStorage.removeItem("multica_token"); localStorage.removeItem("multica_workspace_id"); this.token = null; this.workspaceId = null; if (window.location.pathname !== "/login") { window.location.href = "/login"; } } let message = `API error: ${res.status} ${res.statusText}`; try { const data = await res.json() as { error?: string }; if (typeof data.error === "string" && data.error) { message = data.error; } } catch { // Ignore non-JSON error bodies. } this.logger.error(`← ${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms`, error: message }); throw new Error(message); } this.logger.info(`← ${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms` }); // Handle 204 No Content if (res.status === 204) { return undefined as T; } return res.json() as Promise; } // Auth async sendCode(email: string): Promise { await this.fetch("/auth/send-code", { method: "POST", body: JSON.stringify({ email }), }); } async verifyCode(email: string, code: string): Promise { return this.fetch("/auth/verify-code", { method: "POST", body: JSON.stringify({ email, code }), }); } async getMe(): Promise { return this.fetch("/api/me"); } async updateMe(data: UpdateMeRequest): Promise { return this.fetch("/api/me", { method: "PATCH", body: JSON.stringify(data), }); } // Issues async listIssues(params?: ListIssuesParams): Promise { const search = new URLSearchParams(); if (params?.limit) search.set("limit", String(params.limit)); if (params?.offset) search.set("offset", String(params.offset)); const wsId = params?.workspace_id ?? this.workspaceId; if (wsId) search.set("workspace_id", wsId); if (params?.status) search.set("status", params.status); if (params?.priority) search.set("priority", params.priority); if (params?.assignee_id) search.set("assignee_id", params.assignee_id); return this.fetch(`/api/issues?${search}`); } async getIssue(id: string): Promise { return this.fetch(`/api/issues/${id}`); } async createIssue(data: CreateIssueRequest): Promise { const search = new URLSearchParams(); if (this.workspaceId) search.set("workspace_id", this.workspaceId); return this.fetch(`/api/issues?${search}`, { method: "POST", body: JSON.stringify(data), }); } async updateIssue(id: string, data: UpdateIssueRequest): Promise { return this.fetch(`/api/issues/${id}`, { method: "PUT", body: JSON.stringify(data), }); } async deleteIssue(id: string): Promise { await this.fetch(`/api/issues/${id}`, { method: "DELETE" }); } async batchUpdateIssues(issueIds: string[], updates: UpdateIssueRequest): Promise<{ updated: number }> { return this.fetch("/api/issues/batch-update", { method: "POST", body: JSON.stringify({ issue_ids: issueIds, updates }), }); } async batchDeleteIssues(issueIds: string[]): Promise<{ deleted: number }> { return this.fetch("/api/issues/batch-delete", { method: "POST", body: JSON.stringify({ issue_ids: issueIds }), }); } // Comments async listComments(issueId: string): Promise { return this.fetch(`/api/issues/${issueId}/comments`); } async createComment(issueId: string, content: string, type?: string, parentId?: string): Promise { return this.fetch(`/api/issues/${issueId}/comments`, { method: "POST", body: JSON.stringify({ content, type: type ?? "comment", ...(parentId ? { parent_id: parentId } : {}), }), }); } async listTimeline(issueId: string): Promise { return this.fetch(`/api/issues/${issueId}/timeline`); } async updateComment(commentId: string, content: string): Promise { return this.fetch(`/api/comments/${commentId}`, { method: "PUT", body: JSON.stringify({ content }), }); } async deleteComment(commentId: string): Promise { await this.fetch(`/api/comments/${commentId}`, { method: "DELETE" }); } // Subscribers async listIssueSubscribers(issueId: string): Promise { return this.fetch(`/api/issues/${issueId}/subscribers`); } async subscribeToIssue(issueId: string, userId?: string, userType?: string): Promise { const body: Record = {}; if (userId) body.user_id = userId; if (userType) body.user_type = userType; await this.fetch(`/api/issues/${issueId}/subscribe`, { method: "POST", body: JSON.stringify(body), }); } async unsubscribeFromIssue(issueId: string, userId?: string, userType?: string): Promise { const body: Record = {}; if (userId) body.user_id = userId; if (userType) body.user_type = userType; await this.fetch(`/api/issues/${issueId}/unsubscribe`, { method: "POST", body: JSON.stringify(body), }); } // Agents async listAgents(params?: { workspace_id?: string }): Promise { const search = new URLSearchParams(); const wsId = params?.workspace_id ?? this.workspaceId; if (wsId) search.set("workspace_id", wsId); return this.fetch(`/api/agents?${search}`); } async getAgent(id: string): Promise { return this.fetch(`/api/agents/${id}`); } async createAgent(data: CreateAgentRequest): Promise { return this.fetch("/api/agents", { method: "POST", body: JSON.stringify(data), }); } async updateAgent(id: string, data: UpdateAgentRequest): Promise { return this.fetch(`/api/agents/${id}`, { method: "PUT", body: JSON.stringify(data), }); } async deleteAgent(id: string): Promise { await this.fetch(`/api/agents/${id}`, { method: "DELETE" }); } async listRuntimes(params?: { workspace_id?: string }): Promise { const search = new URLSearchParams(); const wsId = params?.workspace_id ?? this.workspaceId; if (wsId) search.set("workspace_id", wsId); return this.fetch(`/api/runtimes?${search}`); } async getRuntimeUsage(runtimeId: string, params?: { days?: number }): Promise { const search = new URLSearchParams(); if (params?.days) search.set("days", String(params.days)); return this.fetch(`/api/runtimes/${runtimeId}/usage?${search}`); } async getRuntimeTaskActivity(runtimeId: string): Promise { return this.fetch(`/api/runtimes/${runtimeId}/activity`); } async pingRuntime(runtimeId: string): Promise { return this.fetch(`/api/runtimes/${runtimeId}/ping`, { method: "POST" }); } async getPingResult(runtimeId: string, pingId: string): Promise { return this.fetch(`/api/runtimes/${runtimeId}/ping/${pingId}`); } async listAgentTasks(agentId: string): Promise { return this.fetch(`/api/agents/${agentId}/tasks`); } async getActiveTaskForIssue(issueId: string): Promise<{ task: AgentTask | null }> { return this.fetch(`/api/issues/${issueId}/active-task`); } async listTaskMessages(taskId: string): Promise { return this.fetch(`/api/daemon/tasks/${taskId}/messages`); } async getDaemonPairingSession(token: string): Promise { return this.fetch(`/api/daemon/pairing-sessions/${token}`); } async approveDaemonPairingSession( token: string, data: ApproveDaemonPairingSessionRequest, ): Promise { return this.fetch(`/api/daemon/pairing-sessions/${token}/approve`, { method: "POST", body: JSON.stringify(data), }); } // Inbox async listInbox(): Promise { return this.fetch("/api/inbox"); } async markInboxRead(id: string): Promise { return this.fetch(`/api/inbox/${id}/read`, { method: "POST" }); } async archiveInbox(id: string): Promise { return this.fetch(`/api/inbox/${id}/archive`, { method: "POST" }); } async getUnreadInboxCount(): Promise<{ count: number }> { return this.fetch("/api/inbox/unread-count"); } async markAllInboxRead(): Promise<{ count: number }> { return this.fetch("/api/inbox/mark-all-read", { method: "POST" }); } async archiveAllInbox(): Promise<{ count: number }> { return this.fetch("/api/inbox/archive-all", { method: "POST" }); } async archiveAllReadInbox(): Promise<{ count: number }> { return this.fetch("/api/inbox/archive-all-read", { method: "POST" }); } async archiveCompletedInbox(): Promise<{ count: number }> { return this.fetch("/api/inbox/archive-completed", { method: "POST" }); } // Workspaces async listWorkspaces(): Promise { return this.fetch("/api/workspaces"); } async getWorkspace(id: string): Promise { return this.fetch(`/api/workspaces/${id}`); } async createWorkspace(data: { name: string; slug: string; description?: string; context?: string }): Promise { return this.fetch("/api/workspaces", { method: "POST", body: JSON.stringify(data), }); } async updateWorkspace(id: string, data: { name?: string; description?: string; context?: string; settings?: Record; repos?: WorkspaceRepo[] }): Promise { return this.fetch(`/api/workspaces/${id}`, { method: "PATCH", body: JSON.stringify(data), }); } // Members async listMembers(workspaceId: string): Promise { return this.fetch(`/api/workspaces/${workspaceId}/members`); } async createMember(workspaceId: string, data: CreateMemberRequest): Promise { return this.fetch(`/api/workspaces/${workspaceId}/members`, { method: "POST", body: JSON.stringify(data), }); } async updateMember(workspaceId: string, memberId: string, data: UpdateMemberRequest): Promise { return this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, { method: "PATCH", body: JSON.stringify(data), }); } async deleteMember(workspaceId: string, memberId: string): Promise { await this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, { method: "DELETE", }); } async leaveWorkspace(workspaceId: string): Promise { await this.fetch(`/api/workspaces/${workspaceId}/leave`, { method: "POST", }); } async deleteWorkspace(workspaceId: string): Promise { await this.fetch(`/api/workspaces/${workspaceId}`, { method: "DELETE", }); } // Skills async listSkills(): Promise { return this.fetch("/api/skills"); } async getSkill(id: string): Promise { return this.fetch(`/api/skills/${id}`); } async createSkill(data: CreateSkillRequest): Promise { return this.fetch("/api/skills", { method: "POST", body: JSON.stringify(data), }); } async updateSkill(id: string, data: UpdateSkillRequest): Promise { return this.fetch(`/api/skills/${id}`, { method: "PUT", body: JSON.stringify(data), }); } async deleteSkill(id: string): Promise { await this.fetch(`/api/skills/${id}`, { method: "DELETE" }); } async importSkill(data: { url: string }): Promise { return this.fetch("/api/skills/import", { method: "POST", body: JSON.stringify(data), }); } async listAgentSkills(agentId: string): Promise { return this.fetch(`/api/agents/${agentId}/skills`); } async setAgentSkills(agentId: string, data: SetAgentSkillsRequest): Promise { await this.fetch(`/api/agents/${agentId}/skills`, { method: "PUT", body: JSON.stringify(data), }); } // Personal Access Tokens async listPersonalAccessTokens(): Promise { return this.fetch("/api/tokens"); } async createPersonalAccessToken(data: CreatePersonalAccessTokenRequest): Promise { return this.fetch("/api/tokens", { method: "POST", body: JSON.stringify(data), }); } async revokePersonalAccessToken(id: string): Promise { await this.fetch(`/api/tokens/${id}`, { method: "DELETE" }); } }