mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-13 05:16:29 +02:00
The frontend's listTaskMessages() was calling /api/daemon/tasks/{id}/messages
which uses DaemonAuth middleware (requires Authorization header). After the
cookie auth migration (#819), cookie-mode sessions don't send an Authorization
header, causing 401 on this endpoint. The 401 then triggers handleUnauthorized()
which clears the workspace context, cascading into 400 errors on all subsequent
requests.
Fix: add GET /api/tasks/{taskId}/messages under regular user auth middleware,
and update the frontend to use it instead of the daemon endpoint.
Closes #833
748 lines
23 KiB
TypeScript
748 lines
23 KiB
TypeScript
import type {
|
|
Issue,
|
|
CreateIssueRequest,
|
|
UpdateIssueRequest,
|
|
ListIssuesResponse,
|
|
SearchIssuesResponse,
|
|
SearchProjectsResponse,
|
|
UpdateMeRequest,
|
|
CreateMemberRequest,
|
|
UpdateMemberRequest,
|
|
ListIssuesParams,
|
|
Agent,
|
|
CreateAgentRequest,
|
|
UpdateAgentRequest,
|
|
AgentTask,
|
|
AgentRuntime,
|
|
InboxItem,
|
|
IssueSubscriber,
|
|
Comment,
|
|
Reaction,
|
|
IssueReaction,
|
|
Workspace,
|
|
WorkspaceRepo,
|
|
MemberWithUser,
|
|
User,
|
|
Skill,
|
|
CreateSkillRequest,
|
|
UpdateSkillRequest,
|
|
SetAgentSkillsRequest,
|
|
PersonalAccessToken,
|
|
CreatePersonalAccessTokenRequest,
|
|
CreatePersonalAccessTokenResponse,
|
|
RuntimeUsage,
|
|
IssueUsageSummary,
|
|
RuntimeHourlyActivity,
|
|
RuntimePing,
|
|
RuntimeUpdate,
|
|
TimelineEntry,
|
|
AssigneeFrequencyEntry,
|
|
TaskMessagePayload,
|
|
Attachment,
|
|
ChatSession,
|
|
ChatMessage,
|
|
SendChatMessageResponse,
|
|
Project,
|
|
CreateProjectRequest,
|
|
UpdateProjectRequest,
|
|
ListProjectsResponse,
|
|
PinnedItem,
|
|
CreatePinRequest,
|
|
PinnedItemType,
|
|
ReorderPinsRequest,
|
|
} from "../types";
|
|
import { type Logger, noopLogger } from "../logger";
|
|
import { createRequestId } from "../utils";
|
|
|
|
export interface ApiClientOptions {
|
|
logger?: Logger;
|
|
onUnauthorized?: () => void;
|
|
}
|
|
|
|
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;
|
|
private options: ApiClientOptions;
|
|
|
|
constructor(baseUrl: string, options?: ApiClientOptions) {
|
|
this.baseUrl = baseUrl;
|
|
this.options = options ?? {};
|
|
this.logger = options?.logger ?? noopLogger;
|
|
}
|
|
|
|
setToken(token: string | null) {
|
|
this.token = token;
|
|
}
|
|
|
|
setWorkspaceId(id: string | null) {
|
|
this.workspaceId = id;
|
|
}
|
|
|
|
private readCsrfToken(): string | null {
|
|
if (typeof document === "undefined") return null;
|
|
const match = document.cookie
|
|
.split("; ")
|
|
.find((c) => c.startsWith("multica_csrf="));
|
|
return match ? match.split("=")[1] ?? null : null;
|
|
}
|
|
|
|
private authHeaders(): Record<string, string> {
|
|
const headers: Record<string, string> = {};
|
|
if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
|
|
if (this.workspaceId) headers["X-Workspace-ID"] = this.workspaceId;
|
|
const csrf = this.readCsrfToken();
|
|
if (csrf) headers["X-CSRF-Token"] = csrf;
|
|
return headers;
|
|
}
|
|
|
|
private handleUnauthorized() {
|
|
this.token = null;
|
|
this.workspaceId = null;
|
|
this.options.onUnauthorized?.();
|
|
}
|
|
|
|
private async parseErrorMessage(res: Response, fallback: string): Promise<string> {
|
|
try {
|
|
const data = await res.json() as { error?: string };
|
|
if (typeof data.error === "string" && data.error) return data.error;
|
|
} catch {
|
|
// Ignore non-JSON error bodies.
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const rid = createRequestId();
|
|
const start = Date.now();
|
|
const method = init?.method ?? "GET";
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
"X-Request-ID": rid,
|
|
...this.authHeaders(),
|
|
...((init?.headers as Record<string, string>) ?? {}),
|
|
};
|
|
|
|
this.logger.info(`→ ${method} ${path}`, { rid });
|
|
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
...init,
|
|
headers,
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) this.handleUnauthorized();
|
|
const message = await this.parseErrorMessage(res, `API error: ${res.status} ${res.statusText}`);
|
|
const logLevel = res.status === 404 ? "warn" : "error";
|
|
this.logger[logLevel](`← ${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<T>;
|
|
}
|
|
|
|
// Auth
|
|
async sendCode(email: string): Promise<void> {
|
|
await this.fetch("/auth/send-code", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
}
|
|
|
|
async verifyCode(email: string, code: string): Promise<LoginResponse> {
|
|
return this.fetch("/auth/verify-code", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, code }),
|
|
});
|
|
}
|
|
|
|
async googleLogin(code: string, redirectUri: string): Promise<LoginResponse> {
|
|
return this.fetch("/auth/google", {
|
|
method: "POST",
|
|
body: JSON.stringify({ code, redirect_uri: redirectUri }),
|
|
});
|
|
}
|
|
|
|
async logout(): Promise<void> {
|
|
await this.fetch("/auth/logout", { method: "POST" });
|
|
}
|
|
|
|
async getMe(): Promise<User> {
|
|
return this.fetch("/api/me");
|
|
}
|
|
|
|
async updateMe(data: UpdateMeRequest): Promise<User> {
|
|
return this.fetch("/api/me", {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// Issues
|
|
async listIssues(params?: ListIssuesParams): Promise<ListIssuesResponse> {
|
|
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);
|
|
if (params?.assignee_ids?.length) search.set("assignee_ids", params.assignee_ids.join(","));
|
|
if (params?.creator_id) search.set("creator_id", params.creator_id);
|
|
if (params?.open_only) search.set("open_only", "true");
|
|
return this.fetch(`/api/issues?${search}`);
|
|
}
|
|
|
|
async searchIssues(params: { q: string; limit?: number; offset?: number; include_closed?: boolean; signal?: AbortSignal }): Promise<SearchIssuesResponse> {
|
|
const search = new URLSearchParams({ q: params.q });
|
|
if (params.limit !== undefined) search.set("limit", String(params.limit));
|
|
if (params.offset !== undefined) search.set("offset", String(params.offset));
|
|
if (params.include_closed) search.set("include_closed", "true");
|
|
return this.fetch(`/api/issues/search?${search}`, params.signal ? { signal: params.signal } : undefined);
|
|
}
|
|
|
|
async searchProjects(params: { q: string; limit?: number; offset?: number; include_closed?: boolean; signal?: AbortSignal }): Promise<SearchProjectsResponse> {
|
|
const search = new URLSearchParams({ q: params.q });
|
|
if (params.limit !== undefined) search.set("limit", String(params.limit));
|
|
if (params.offset !== undefined) search.set("offset", String(params.offset));
|
|
if (params.include_closed) search.set("include_closed", "true");
|
|
return this.fetch(`/api/projects/search?${search}`, params.signal ? { signal: params.signal } : undefined);
|
|
}
|
|
|
|
async getIssue(id: string): Promise<Issue> {
|
|
return this.fetch(`/api/issues/${id}`);
|
|
}
|
|
|
|
async createIssue(data: CreateIssueRequest): Promise<Issue> {
|
|
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<Issue> {
|
|
return this.fetch(`/api/issues/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async listChildIssues(id: string): Promise<{ issues: Issue[] }> {
|
|
return this.fetch(`/api/issues/${id}/children`);
|
|
}
|
|
|
|
async deleteIssue(id: string): Promise<void> {
|
|
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<Comment[]> {
|
|
return this.fetch(`/api/issues/${issueId}/comments`);
|
|
}
|
|
|
|
async createComment(issueId: string, content: string, type?: string, parentId?: string, attachmentIds?: string[]): Promise<Comment> {
|
|
return this.fetch(`/api/issues/${issueId}/comments`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
content,
|
|
type: type ?? "comment",
|
|
...(parentId ? { parent_id: parentId } : {}),
|
|
...(attachmentIds?.length ? { attachment_ids: attachmentIds } : {}),
|
|
}),
|
|
});
|
|
}
|
|
|
|
async listTimeline(issueId: string): Promise<TimelineEntry[]> {
|
|
return this.fetch(`/api/issues/${issueId}/timeline`);
|
|
}
|
|
|
|
async getAssigneeFrequency(): Promise<AssigneeFrequencyEntry[]> {
|
|
return this.fetch("/api/assignee-frequency");
|
|
}
|
|
|
|
async updateComment(commentId: string, content: string): Promise<Comment> {
|
|
return this.fetch(`/api/comments/${commentId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
}
|
|
|
|
async deleteComment(commentId: string): Promise<void> {
|
|
await this.fetch(`/api/comments/${commentId}`, { method: "DELETE" });
|
|
}
|
|
|
|
async addReaction(commentId: string, emoji: string): Promise<Reaction> {
|
|
return this.fetch(`/api/comments/${commentId}/reactions`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
async removeReaction(commentId: string, emoji: string): Promise<void> {
|
|
await this.fetch(`/api/comments/${commentId}/reactions`, {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
async addIssueReaction(issueId: string, emoji: string): Promise<IssueReaction> {
|
|
return this.fetch(`/api/issues/${issueId}/reactions`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
async removeIssueReaction(issueId: string, emoji: string): Promise<void> {
|
|
await this.fetch(`/api/issues/${issueId}/reactions`, {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
// Subscribers
|
|
async listIssueSubscribers(issueId: string): Promise<IssueSubscriber[]> {
|
|
return this.fetch(`/api/issues/${issueId}/subscribers`);
|
|
}
|
|
|
|
async subscribeToIssue(issueId: string, userId?: string, userType?: string): Promise<void> {
|
|
const body: Record<string, string> = {};
|
|
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<void> {
|
|
const body: Record<string, string> = {};
|
|
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; include_archived?: boolean }): Promise<Agent[]> {
|
|
const search = new URLSearchParams();
|
|
const wsId = params?.workspace_id ?? this.workspaceId;
|
|
if (wsId) search.set("workspace_id", wsId);
|
|
if (params?.include_archived) search.set("include_archived", "true");
|
|
return this.fetch(`/api/agents?${search}`);
|
|
}
|
|
|
|
async getAgent(id: string): Promise<Agent> {
|
|
return this.fetch(`/api/agents/${id}`);
|
|
}
|
|
|
|
async createAgent(data: CreateAgentRequest): Promise<Agent> {
|
|
return this.fetch("/api/agents", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateAgent(id: string, data: UpdateAgentRequest): Promise<Agent> {
|
|
return this.fetch(`/api/agents/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async archiveAgent(id: string): Promise<Agent> {
|
|
return this.fetch(`/api/agents/${id}/archive`, { method: "POST" });
|
|
}
|
|
|
|
async restoreAgent(id: string): Promise<Agent> {
|
|
return this.fetch(`/api/agents/${id}/restore`, { method: "POST" });
|
|
}
|
|
|
|
async listRuntimes(params?: { workspace_id?: string; owner?: "me" }): Promise<AgentRuntime[]> {
|
|
const search = new URLSearchParams();
|
|
const wsId = params?.workspace_id ?? this.workspaceId;
|
|
if (wsId) search.set("workspace_id", wsId);
|
|
if (params?.owner) search.set("owner", params.owner);
|
|
return this.fetch(`/api/runtimes?${search}`);
|
|
}
|
|
|
|
async deleteRuntime(runtimeId: string): Promise<void> {
|
|
await this.fetch(`/api/runtimes/${runtimeId}`, { method: "DELETE" });
|
|
}
|
|
|
|
async getRuntimeUsage(runtimeId: string, params?: { days?: number }): Promise<RuntimeUsage[]> {
|
|
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<RuntimeHourlyActivity[]> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/activity`);
|
|
}
|
|
|
|
async pingRuntime(runtimeId: string): Promise<RuntimePing> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/ping`, { method: "POST" });
|
|
}
|
|
|
|
async getPingResult(runtimeId: string, pingId: string): Promise<RuntimePing> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/ping/${pingId}`);
|
|
}
|
|
|
|
async initiateUpdate(
|
|
runtimeId: string,
|
|
targetVersion: string,
|
|
): Promise<RuntimeUpdate> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/update`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ target_version: targetVersion }),
|
|
});
|
|
}
|
|
|
|
async getUpdateResult(
|
|
runtimeId: string,
|
|
updateId: string,
|
|
): Promise<RuntimeUpdate> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/update/${updateId}`);
|
|
}
|
|
|
|
async listAgentTasks(agentId: string): Promise<AgentTask[]> {
|
|
return this.fetch(`/api/agents/${agentId}/tasks`);
|
|
}
|
|
|
|
async getActiveTasksForIssue(issueId: string): Promise<{ tasks: AgentTask[] }> {
|
|
return this.fetch(`/api/issues/${issueId}/active-task`);
|
|
}
|
|
|
|
async listTaskMessages(taskId: string): Promise<TaskMessagePayload[]> {
|
|
return this.fetch(`/api/tasks/${taskId}/messages`);
|
|
}
|
|
|
|
async listTasksByIssue(issueId: string): Promise<AgentTask[]> {
|
|
return this.fetch(`/api/issues/${issueId}/task-runs`);
|
|
}
|
|
|
|
async getIssueUsage(issueId: string): Promise<IssueUsageSummary> {
|
|
return this.fetch(`/api/issues/${issueId}/usage`);
|
|
}
|
|
|
|
async cancelTask(issueId: string, taskId: string): Promise<AgentTask> {
|
|
return this.fetch(`/api/issues/${issueId}/tasks/${taskId}/cancel`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
// Inbox
|
|
async listInbox(): Promise<InboxItem[]> {
|
|
return this.fetch("/api/inbox");
|
|
}
|
|
|
|
async markInboxRead(id: string): Promise<InboxItem> {
|
|
return this.fetch(`/api/inbox/${id}/read`, { method: "POST" });
|
|
}
|
|
|
|
async archiveInbox(id: string): Promise<InboxItem> {
|
|
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<Workspace[]> {
|
|
return this.fetch("/api/workspaces");
|
|
}
|
|
|
|
async getWorkspace(id: string): Promise<Workspace> {
|
|
return this.fetch(`/api/workspaces/${id}`);
|
|
}
|
|
|
|
async createWorkspace(data: { name: string; slug: string; description?: string; context?: string }): Promise<Workspace> {
|
|
return this.fetch("/api/workspaces", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateWorkspace(id: string, data: { name?: string; description?: string; context?: string; settings?: Record<string, unknown>; repos?: WorkspaceRepo[] }): Promise<Workspace> {
|
|
return this.fetch(`/api/workspaces/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// Members
|
|
async listMembers(workspaceId: string): Promise<MemberWithUser[]> {
|
|
return this.fetch(`/api/workspaces/${workspaceId}/members`);
|
|
}
|
|
|
|
async createMember(workspaceId: string, data: CreateMemberRequest): Promise<MemberWithUser> {
|
|
return this.fetch(`/api/workspaces/${workspaceId}/members`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateMember(workspaceId: string, memberId: string, data: UpdateMemberRequest): Promise<MemberWithUser> {
|
|
return this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteMember(workspaceId: string, memberId: string): Promise<void> {
|
|
await this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
async leaveWorkspace(workspaceId: string): Promise<void> {
|
|
await this.fetch(`/api/workspaces/${workspaceId}/leave`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
async deleteWorkspace(workspaceId: string): Promise<void> {
|
|
await this.fetch(`/api/workspaces/${workspaceId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// Skills
|
|
async listSkills(): Promise<Skill[]> {
|
|
return this.fetch("/api/skills");
|
|
}
|
|
|
|
async getSkill(id: string): Promise<Skill> {
|
|
return this.fetch(`/api/skills/${id}`);
|
|
}
|
|
|
|
async createSkill(data: CreateSkillRequest): Promise<Skill> {
|
|
return this.fetch("/api/skills", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateSkill(id: string, data: UpdateSkillRequest): Promise<Skill> {
|
|
return this.fetch(`/api/skills/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteSkill(id: string): Promise<void> {
|
|
await this.fetch(`/api/skills/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
async importSkill(data: { url: string }): Promise<Skill> {
|
|
return this.fetch("/api/skills/import", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async listAgentSkills(agentId: string): Promise<Skill[]> {
|
|
return this.fetch(`/api/agents/${agentId}/skills`);
|
|
}
|
|
|
|
async setAgentSkills(agentId: string, data: SetAgentSkillsRequest): Promise<void> {
|
|
await this.fetch(`/api/agents/${agentId}/skills`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// Personal Access Tokens
|
|
async listPersonalAccessTokens(): Promise<PersonalAccessToken[]> {
|
|
return this.fetch("/api/tokens");
|
|
}
|
|
|
|
async createPersonalAccessToken(data: CreatePersonalAccessTokenRequest): Promise<CreatePersonalAccessTokenResponse> {
|
|
return this.fetch("/api/tokens", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async revokePersonalAccessToken(id: string): Promise<void> {
|
|
await this.fetch(`/api/tokens/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
// File Upload & Attachments
|
|
async uploadFile(file: File, opts?: { issueId?: string; commentId?: string }): Promise<Attachment> {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
if (opts?.issueId) formData.append("issue_id", opts.issueId);
|
|
if (opts?.commentId) formData.append("comment_id", opts.commentId);
|
|
|
|
const rid = createRequestId();
|
|
const start = Date.now();
|
|
this.logger.info("→ POST /api/upload-file", { rid });
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/upload-file`, {
|
|
method: "POST",
|
|
headers: this.authHeaders(),
|
|
body: formData,
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) this.handleUnauthorized();
|
|
const message = await this.parseErrorMessage(res, `Upload failed: ${res.status}`);
|
|
this.logger.error(`← ${res.status} /api/upload-file`, { rid, duration: `${Date.now() - start}ms`, error: message });
|
|
throw new Error(message);
|
|
}
|
|
|
|
this.logger.info(`← ${res.status} /api/upload-file`, { rid, duration: `${Date.now() - start}ms` });
|
|
return res.json() as Promise<Attachment>;
|
|
}
|
|
|
|
// Chat Sessions
|
|
async listChatSessions(params?: { status?: string }): Promise<ChatSession[]> {
|
|
const query = params?.status ? `?status=${params.status}` : "";
|
|
return this.fetch(`/api/chat/sessions${query}`);
|
|
}
|
|
|
|
async getChatSession(id: string): Promise<ChatSession> {
|
|
return this.fetch(`/api/chat/sessions/${id}`);
|
|
}
|
|
|
|
async createChatSession(data: { agent_id: string; title?: string }): Promise<ChatSession> {
|
|
return this.fetch("/api/chat/sessions", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async archiveChatSession(id: string): Promise<void> {
|
|
await this.fetch(`/api/chat/sessions/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
async listChatMessages(sessionId: string): Promise<ChatMessage[]> {
|
|
return this.fetch(`/api/chat/sessions/${sessionId}/messages`);
|
|
}
|
|
|
|
async sendChatMessage(sessionId: string, content: string): Promise<SendChatMessageResponse> {
|
|
return this.fetch(`/api/chat/sessions/${sessionId}/messages`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
}
|
|
|
|
async cancelTaskById(taskId: string): Promise<void> {
|
|
await this.fetch(`/api/tasks/${taskId}/cancel`, { method: "POST" });
|
|
}
|
|
|
|
async listAttachments(issueId: string): Promise<Attachment[]> {
|
|
return this.fetch(`/api/issues/${issueId}/attachments`);
|
|
}
|
|
|
|
async deleteAttachment(id: string): Promise<void> {
|
|
await this.fetch(`/api/attachments/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
// Projects
|
|
async listProjects(params?: { status?: string }): Promise<ListProjectsResponse> {
|
|
const search = new URLSearchParams();
|
|
if (params?.status) search.set("status", params.status);
|
|
return this.fetch(`/api/projects?${search}`);
|
|
}
|
|
|
|
async getProject(id: string): Promise<Project> {
|
|
return this.fetch(`/api/projects/${id}`);
|
|
}
|
|
|
|
async createProject(data: CreateProjectRequest): Promise<Project> {
|
|
const search = new URLSearchParams();
|
|
if (this.workspaceId) search.set("workspace_id", this.workspaceId);
|
|
return this.fetch(`/api/projects?${search}`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateProject(id: string, data: UpdateProjectRequest): Promise<Project> {
|
|
return this.fetch(`/api/projects/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteProject(id: string): Promise<void> {
|
|
await this.fetch(`/api/projects/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
// Pins
|
|
async listPins(): Promise<PinnedItem[]> {
|
|
return this.fetch("/api/pins");
|
|
}
|
|
|
|
async createPin(data: CreatePinRequest): Promise<PinnedItem> {
|
|
return this.fetch("/api/pins", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deletePin(itemType: PinnedItemType, itemId: string): Promise<void> {
|
|
await this.fetch(`/api/pins/${itemType}/${itemId}`, { method: "DELETE" });
|
|
}
|
|
|
|
async reorderPins(data: ReorderPinsRequest): Promise<void> {
|
|
await this.fetch("/api/pins/reorder", {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
}
|