mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 07:10:49 +02:00
* feat(quick-create): add project picker that remembers last pick Quick-create users targeting one project repeatedly had to restate "in project X" in every prompt. The modal now exposes a project picker beside the agent picker, persists the selection per-workspace, and pins the agent's `multica issue create` invocation to that project so the prompt text doesn't have to. The picked project also flows to the daemon as ProjectID/ProjectTitle and its github_repo resources override the workspace repo fallback — same treatment issue-bound tasks already get. Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): move project picker into property pill row Reviewer feedback: the picker felt out of place wedged next to the agent header. Move it into a property toolbar row above the footer, reusing the shared `ProjectPicker` + `PillButton` so its placement and styling line up exactly with the manual create panel. This also drops the bespoke dropdown / aria / label strings that were only needed while the picker rendered inline beside "Created by". Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): clear stale persisted project + carry across mode switch Two review-blocking bugs in PR #2321: 1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0 and only cleared local state, leaving lastProjectId pointing at a deleted project. The next open re-seeded the dead UUID and submit hit the server's `project not found` rejection. Gate on the query's `isSuccess` so we can tell "loading" apart from "loaded as empty", and clear both local state and the persisted preference when the selection isn't in the resolved list. 2. ManualCreatePanel's switchToAgent dropped the picked project from the carry payload, so flipping manual → agent silently fell back to the agent panel's own lastProjectId — potentially routing the issue to a different project than the one shown in manual mode. Forward project_id alongside prompt / agent_id, and add a regression test. Co-authored-by: multica-agent <github@multica.ai> * test(quick-create): pass new isExpanded props in stale-project tests Main got an expand button on AgentCreatePanel via #2320 while this branch was open, adding `isExpanded` / `setIsExpanded` to the panel's required props. The two new stale-project tests still passed `{ onClose }` only, which CI's typecheck (run on the main+branch merge) caught while my local run did not. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
1276 lines
40 KiB
TypeScript
1276 lines
40 KiB
TypeScript
import type {
|
|
Issue,
|
|
CreateIssueRequest,
|
|
UpdateIssueRequest,
|
|
ListIssuesResponse,
|
|
SearchIssuesResponse,
|
|
SearchProjectsResponse,
|
|
UpdateMeRequest,
|
|
CreateMemberRequest,
|
|
UpdateMemberRequest,
|
|
ListIssuesParams,
|
|
Agent,
|
|
CreateAgentRequest,
|
|
UpdateAgentRequest,
|
|
AgentTask,
|
|
AgentActivityBucket,
|
|
AgentRunCount,
|
|
AgentRuntime,
|
|
InboxItem,
|
|
IssueSubscriber,
|
|
Comment,
|
|
Reaction,
|
|
IssueReaction,
|
|
Workspace,
|
|
WorkspaceRepo,
|
|
MemberWithUser,
|
|
User,
|
|
Skill,
|
|
SkillSummary,
|
|
CreateSkillRequest,
|
|
UpdateSkillRequest,
|
|
SetAgentSkillsRequest,
|
|
PersonalAccessToken,
|
|
CreatePersonalAccessTokenRequest,
|
|
CreatePersonalAccessTokenResponse,
|
|
RuntimeUsage,
|
|
IssueUsageSummary,
|
|
RuntimeHourlyActivity,
|
|
RuntimeUsageByAgent,
|
|
RuntimeUsageByHour,
|
|
RuntimeUpdate,
|
|
RuntimeModelListRequest,
|
|
RuntimeLocalSkillListRequest,
|
|
CreateRuntimeLocalSkillImportRequest,
|
|
RuntimeLocalSkillImportRequest,
|
|
TimelineEntry,
|
|
AssigneeFrequencyEntry,
|
|
TaskMessagePayload,
|
|
Attachment,
|
|
ChatSession,
|
|
ChatMessage,
|
|
ChatPendingTask,
|
|
PendingChatTasksResponse,
|
|
SendChatMessageResponse,
|
|
Project,
|
|
CreateProjectRequest,
|
|
UpdateProjectRequest,
|
|
ListProjectsResponse,
|
|
ProjectResource,
|
|
CreateProjectResourceRequest,
|
|
ListProjectResourcesResponse,
|
|
Label,
|
|
CreateLabelRequest,
|
|
UpdateLabelRequest,
|
|
ListLabelsResponse,
|
|
IssueLabelsResponse,
|
|
PinnedItem,
|
|
CreatePinRequest,
|
|
PinnedItemType,
|
|
ReorderPinsRequest,
|
|
Invitation,
|
|
Autopilot,
|
|
AutopilotTrigger,
|
|
AutopilotRun,
|
|
CreateAutopilotRequest,
|
|
UpdateAutopilotRequest,
|
|
CreateAutopilotTriggerRequest,
|
|
UpdateAutopilotTriggerRequest,
|
|
ListAutopilotsResponse,
|
|
GetAutopilotResponse,
|
|
ListAutopilotRunsResponse,
|
|
NotificationPreferenceResponse,
|
|
NotificationPreferences,
|
|
} from "../types";
|
|
import type { OnboardingCompletionPath } from "../onboarding/types";
|
|
import { type Logger, noopLogger } from "../logger";
|
|
import { createRequestId } from "../utils";
|
|
import { getCurrentSlug } from "../platform/workspace-storage";
|
|
import { parseWithFallback } from "./schema";
|
|
import {
|
|
ChildIssuesResponseSchema,
|
|
CommentsListSchema,
|
|
EMPTY_LIST_ISSUES_RESPONSE,
|
|
EMPTY_TIMELINE_ENTRIES,
|
|
ListIssuesResponseSchema,
|
|
SubscribersListSchema,
|
|
TimelineEntriesSchema,
|
|
} from "./schemas";
|
|
|
|
/** Identifies the calling client to the server.
|
|
* Sent on every HTTP request as X-Client-Platform / X-Client-Version /
|
|
* X-Client-OS so the backend can log, gate, or split metrics by client.
|
|
* See server/internal/middleware/client.go for the receiving end. */
|
|
export interface ApiClientIdentity {
|
|
/** Logical client kind. Server expects: "web" | "desktop" | "cli" | "daemon". */
|
|
platform?: string;
|
|
/** Client/app version string (e.g. "0.1.0", git tag, commit). */
|
|
version?: string;
|
|
/** Operating system the client is running on: "macos" | "windows" | "linux". */
|
|
os?: string;
|
|
}
|
|
|
|
export interface ApiClientOptions {
|
|
logger?: Logger;
|
|
onUnauthorized?: () => void;
|
|
/** Identifies the client to the server. Sent as X-Client-* headers. */
|
|
identity?: ApiClientIdentity;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
token: string;
|
|
user: User;
|
|
}
|
|
|
|
// --- Starter content (post-onboarding import) -----------------------------
|
|
// Shape mirrors the Go request/response in handler/onboarding.go.
|
|
//
|
|
// The client sends both branches of sub-issues and an unbound welcome
|
|
// issue template (title + description, no `agent_id`). The SERVER picks
|
|
// the branch by inspecting the workspace's agent list inside the
|
|
// import transaction. This removes the client as a trusted decider —
|
|
// even if the client has a stale agent cache or lies, the server uses
|
|
// the DB as source of truth.
|
|
|
|
export interface ImportStarterIssuePayload {
|
|
title: string;
|
|
description: string;
|
|
status: string;
|
|
priority: string;
|
|
/** Server uses `user_id` (per app-wide AssigneePicker convention)
|
|
* as assignee when true. No member_id is threaded through. */
|
|
assign_to_self: boolean;
|
|
}
|
|
|
|
export interface ImportStarterWelcomeIssueTemplate {
|
|
title: string;
|
|
description: string;
|
|
/** Defaults to "high" on server when empty. */
|
|
priority: string;
|
|
}
|
|
|
|
export interface ImportStarterContentPayload {
|
|
workspace_id: string;
|
|
project: { title: string; description: string; icon: string };
|
|
/** Always sent. Server creates it only when an agent exists in the
|
|
* workspace; ignored otherwise. Agent id is picked by the server. */
|
|
welcome_issue_template: ImportStarterWelcomeIssueTemplate;
|
|
/** Used when the workspace has at least one agent. */
|
|
agent_guided_sub_issues: ImportStarterIssuePayload[];
|
|
/** Used when the workspace has zero agents. */
|
|
self_serve_sub_issues: ImportStarterIssuePayload[];
|
|
}
|
|
|
|
export interface ImportStarterContentResponse {
|
|
user: User;
|
|
project_id: string;
|
|
/** Non-null when server took the agent-guided branch. */
|
|
welcome_issue_id: string | null;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number;
|
|
readonly statusText: string;
|
|
// Raw decoded JSON body (when the server returned one). Carries structured
|
|
// error fields like `code` so callers can branch on machine-readable
|
|
// identifiers instead of pattern-matching the human-readable message.
|
|
readonly body?: unknown;
|
|
|
|
constructor(message: string, status: number, statusText: string, body?: unknown) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
this.status = status;
|
|
this.statusText = statusText;
|
|
this.body = body;
|
|
}
|
|
}
|
|
|
|
export class ApiClient {
|
|
private baseUrl: string;
|
|
private token: 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;
|
|
}
|
|
|
|
getBaseUrl(): string {
|
|
return this.baseUrl;
|
|
}
|
|
|
|
setToken(token: string | null) {
|
|
this.token = token;
|
|
}
|
|
|
|
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}`;
|
|
const slug = getCurrentSlug();
|
|
if (slug) headers["X-Workspace-Slug"] = slug;
|
|
const csrf = this.readCsrfToken();
|
|
if (csrf) headers["X-CSRF-Token"] = csrf;
|
|
const id = this.options.identity;
|
|
if (id?.platform) headers["X-Client-Platform"] = id.platform;
|
|
if (id?.version) headers["X-Client-Version"] = id.version;
|
|
if (id?.os) headers["X-Client-OS"] = id.os;
|
|
return headers;
|
|
}
|
|
|
|
private handleUnauthorized() {
|
|
this.token = null;
|
|
// Workspace id is owned by the URL-driven workspace-storage singleton
|
|
// (set by [workspaceSlug]/layout.tsx). On 401, the auth flow navigates
|
|
// to /login which leaves the workspace route, and the next workspace
|
|
// entry will overwrite the id. No clear needed here.
|
|
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;
|
|
}
|
|
|
|
// Reads the response body once for both human-readable error message and
|
|
// structured fields. The Response stream can only be consumed once, so
|
|
// both pieces have to come from a single read.
|
|
private async parseErrorBody(res: Response, fallback: string): Promise<{ message: string; body: unknown }> {
|
|
try {
|
|
const data = await res.json() as { error?: string };
|
|
const message = typeof data.error === "string" && data.error ? data.error : fallback;
|
|
return { message, body: data };
|
|
} catch {
|
|
return { message: fallback, body: undefined };
|
|
}
|
|
}
|
|
|
|
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, body } = await this.parseErrorBody(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 ApiError(message, res.status, res.statusText, body);
|
|
}
|
|
|
|
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 issueCliToken(): Promise<{ token: string }> {
|
|
return this.fetch("/api/cli-token", { method: "POST" });
|
|
}
|
|
|
|
async getMe(): Promise<User> {
|
|
return this.fetch("/api/me");
|
|
}
|
|
|
|
async markOnboardingComplete(payload?: {
|
|
completion_path?: OnboardingCompletionPath;
|
|
workspace_id?: string;
|
|
}): Promise<User> {
|
|
return this.fetch("/api/me/onboarding/complete", {
|
|
method: "POST",
|
|
body: payload ? JSON.stringify(payload) : undefined,
|
|
});
|
|
}
|
|
|
|
async joinCloudWaitlist(payload: {
|
|
email: string;
|
|
reason?: string;
|
|
}): Promise<User> {
|
|
return this.fetch("/api/me/onboarding/cloud-waitlist", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
async patchOnboarding(payload: {
|
|
questionnaire?: Record<string, unknown>;
|
|
}): Promise<User> {
|
|
return this.fetch("/api/me/onboarding", {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Imports the Getting Started project + optional welcome issue + sub-issues
|
|
* in a single server-side transaction. Gated by an atomic
|
|
* starter_content_state: NULL → 'imported' claim — a second call returns
|
|
* 409 (already decided) and creates nothing new.
|
|
*
|
|
* The content templates live in TypeScript (see
|
|
* @multica/views/onboarding/utils/starter-content-templates) and are
|
|
* rendered from the user's questionnaire answers before being sent.
|
|
*/
|
|
async importStarterContent(
|
|
payload: ImportStarterContentPayload,
|
|
): Promise<ImportStarterContentResponse> {
|
|
return this.fetch("/api/me/starter-content/import", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
async dismissStarterContent(payload?: {
|
|
workspace_id?: string;
|
|
}): Promise<User> {
|
|
return this.fetch("/api/me/starter-content/dismiss", {
|
|
method: "POST",
|
|
body: payload ? JSON.stringify(payload) : undefined,
|
|
});
|
|
}
|
|
|
|
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));
|
|
if (params?.workspace_id) search.set("workspace_id", params.workspace_id);
|
|
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?.project_id) search.set("project_id", params.project_id);
|
|
if (params?.open_only) search.set("open_only", "true");
|
|
const path = `/api/issues?${search}`;
|
|
const raw = await this.fetch<unknown>(path);
|
|
return parseWithFallback(raw, ListIssuesResponseSchema, EMPTY_LIST_ISSUES_RESPONSE, {
|
|
endpoint: "GET /api/issues",
|
|
});
|
|
}
|
|
|
|
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> {
|
|
return this.fetch("/api/issues", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async quickCreateIssue(data: { agent_id: string; prompt: string; project_id?: string | null }): Promise<{ task_id: string }> {
|
|
return this.fetch("/api/issues/quick-create", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async createFeedback(data: {
|
|
message: string;
|
|
url?: string;
|
|
workspace_id?: string;
|
|
}): Promise<{ id: string; created_at: string }> {
|
|
return this.fetch("/api/feedback", {
|
|
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[] }> {
|
|
const raw = await this.fetch<unknown>(`/api/issues/${id}/children`);
|
|
return parseWithFallback(raw, ChildIssuesResponseSchema, { issues: [] }, {
|
|
endpoint: "GET /api/issues/:id/children",
|
|
});
|
|
}
|
|
|
|
async getChildIssueProgress(): Promise<{ progress: { parent_issue_id: string; total: number; done: number }[] }> {
|
|
return this.fetch("/api/issues/child-progress");
|
|
}
|
|
|
|
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[]> {
|
|
const raw = await this.fetch<unknown>(`/api/issues/${issueId}/comments`);
|
|
return parseWithFallback(raw, CommentsListSchema, [], {
|
|
endpoint: "GET /api/issues/:id/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[]> {
|
|
const raw = await this.fetch<unknown>(
|
|
`/api/issues/${issueId}/timeline`,
|
|
);
|
|
return parseWithFallback(raw, TimelineEntriesSchema, EMPTY_TIMELINE_ENTRIES, {
|
|
endpoint: "GET /api/issues/:id/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 resolveComment(commentId: string): Promise<Comment> {
|
|
return this.fetch(`/api/comments/${commentId}/resolve`, { method: "POST" });
|
|
}
|
|
|
|
async unresolveComment(commentId: string): Promise<Comment> {
|
|
return this.fetch(`/api/comments/${commentId}/resolve`, { 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[]> {
|
|
const raw = await this.fetch<unknown>(`/api/issues/${issueId}/subscribers`);
|
|
return parseWithFallback(raw, SubscribersListSchema, [], {
|
|
endpoint: "GET /api/issues/:id/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();
|
|
if (params?.workspace_id) search.set("workspace_id", params.workspace_id);
|
|
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" });
|
|
}
|
|
|
|
// Bulk-cancel every active task (queued/dispatched/running) for the agent.
|
|
// Permission: agent owner or workspace admin/owner. Server returns the
|
|
// count of cancelled rows; broadcasts task:cancelled for each so other
|
|
// surfaces can clear their live cards.
|
|
async cancelAgentTasks(id: string): Promise<{ cancelled: number }> {
|
|
return this.fetch(`/api/agents/${id}/cancel-tasks`, { method: "POST" });
|
|
}
|
|
|
|
async listRuntimes(params?: { workspace_id?: string; owner?: "me" }): Promise<AgentRuntime[]> {
|
|
const search = new URLSearchParams();
|
|
if (params?.workspace_id) search.set("workspace_id", params.workspace_id);
|
|
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 getRuntimeUsageByAgent(
|
|
runtimeId: string,
|
|
params?: { days?: number },
|
|
): Promise<RuntimeUsageByAgent[]> {
|
|
const search = new URLSearchParams();
|
|
if (params?.days) search.set("days", String(params.days));
|
|
return this.fetch(`/api/runtimes/${runtimeId}/usage/by-agent?${search}`);
|
|
}
|
|
|
|
async getRuntimeUsageByHour(
|
|
runtimeId: string,
|
|
params?: { days?: number },
|
|
): Promise<RuntimeUsageByHour[]> {
|
|
const search = new URLSearchParams();
|
|
if (params?.days) search.set("days", String(params.days));
|
|
return this.fetch(`/api/runtimes/${runtimeId}/usage/by-hour?${search}`);
|
|
}
|
|
|
|
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 initiateListModels(runtimeId: string): Promise<RuntimeModelListRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/models`, { method: "POST" });
|
|
}
|
|
|
|
async getListModelsResult(
|
|
runtimeId: string,
|
|
requestId: string,
|
|
): Promise<RuntimeModelListRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/models/${requestId}`);
|
|
}
|
|
|
|
async initiateListLocalSkills(
|
|
runtimeId: string,
|
|
): Promise<RuntimeLocalSkillListRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/local-skills`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
async getListLocalSkillsResult(
|
|
runtimeId: string,
|
|
requestId: string,
|
|
): Promise<RuntimeLocalSkillListRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/local-skills/${requestId}`);
|
|
}
|
|
|
|
async initiateImportLocalSkill(
|
|
runtimeId: string,
|
|
data: CreateRuntimeLocalSkillImportRequest,
|
|
): Promise<RuntimeLocalSkillImportRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/local-skills/import`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async getImportLocalSkillResult(
|
|
runtimeId: string,
|
|
requestId: string,
|
|
): Promise<RuntimeLocalSkillImportRequest> {
|
|
return this.fetch(`/api/runtimes/${runtimeId}/local-skills/import/${requestId}`);
|
|
}
|
|
|
|
async listAgentTasks(agentId: string): Promise<AgentTask[]> {
|
|
return this.fetch(`/api/agents/${agentId}/tasks`);
|
|
}
|
|
|
|
// Workspace-scoped agent task snapshot: every active task
|
|
// (queued/dispatched/running) plus each agent's most recent terminal task.
|
|
// Powers the front-end's "active wins, else latest terminal" presence
|
|
// derivation; one fetch backs every per-agent presence read in the app.
|
|
// Workspace is resolved server-side from the X-Workspace-Slug header.
|
|
async getAgentTaskSnapshot(): Promise<AgentTask[]> {
|
|
return this.fetch(`/api/agent-task-snapshot`);
|
|
}
|
|
|
|
// Per-agent daily activity for the last 30 days, anchored on
|
|
// completed_at. One workspace-wide fetch backs both the Agents-list
|
|
// sparkline (uses trailing 7 buckets) and the agent detail "Last 30
|
|
// days" panel (uses all 30).
|
|
async getWorkspaceAgentActivity30d(): Promise<AgentActivityBucket[]> {
|
|
return this.fetch(`/api/agent-activity-30d`);
|
|
}
|
|
|
|
// Per-agent 30-day total run count for the Agents-list RUNS column.
|
|
async getWorkspaceAgentRunCounts(): Promise<AgentRunCount[]> {
|
|
return this.fetch(`/api/agent-run-counts`);
|
|
}
|
|
|
|
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",
|
|
});
|
|
}
|
|
|
|
async rerunIssue(issueId: string): Promise<AgentTask> {
|
|
return this.fetch(`/api/issues/${issueId}/rerun`, {
|
|
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" });
|
|
}
|
|
|
|
// Notification preferences
|
|
async getNotificationPreferences(): Promise<NotificationPreferenceResponse> {
|
|
return this.fetch("/api/notification-preferences");
|
|
}
|
|
|
|
async updateNotificationPreferences(preferences: NotificationPreferences): Promise<NotificationPreferenceResponse> {
|
|
return this.fetch("/api/notification-preferences", {
|
|
method: "PUT",
|
|
body: JSON.stringify({ preferences }),
|
|
});
|
|
}
|
|
|
|
// App Config
|
|
async getConfig(): Promise<{
|
|
cdn_domain: string;
|
|
allow_signup: boolean;
|
|
google_client_id?: string;
|
|
posthog_key?: string;
|
|
posthog_host?: string;
|
|
analytics_environment?: string;
|
|
}> {
|
|
return this.fetch("/api/config");
|
|
}
|
|
|
|
// 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<Invitation> {
|
|
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",
|
|
});
|
|
}
|
|
|
|
// Invitations
|
|
async listWorkspaceInvitations(workspaceId: string): Promise<Invitation[]> {
|
|
return this.fetch(`/api/workspaces/${workspaceId}/invitations`);
|
|
}
|
|
|
|
async revokeInvitation(workspaceId: string, invitationId: string): Promise<void> {
|
|
await this.fetch(`/api/workspaces/${workspaceId}/invitations/${invitationId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
async listMyInvitations(): Promise<Invitation[]> {
|
|
return this.fetch("/api/invitations");
|
|
}
|
|
|
|
async getInvitation(invitationId: string): Promise<Invitation> {
|
|
return this.fetch(`/api/invitations/${invitationId}`);
|
|
}
|
|
|
|
async acceptInvitation(invitationId: string): Promise<MemberWithUser> {
|
|
return this.fetch(`/api/invitations/${invitationId}/accept`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
async declineInvitation(invitationId: string): Promise<void> {
|
|
await this.fetch(`/api/invitations/${invitationId}/decline`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
async deleteWorkspace(workspaceId: string): Promise<void> {
|
|
await this.fetch(`/api/workspaces/${workspaceId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// Skills
|
|
async listSkills(): Promise<SkillSummary[]> {
|
|
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<SkillSummary[]> {
|
|
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 deleteChatSession(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 getPendingChatTask(sessionId: string): Promise<ChatPendingTask> {
|
|
return this.fetch(`/api/chat/sessions/${sessionId}/pending-task`);
|
|
}
|
|
|
|
async listPendingChatTasks(): Promise<PendingChatTasksResponse> {
|
|
return this.fetch(`/api/chat/pending-tasks`);
|
|
}
|
|
|
|
async markChatSessionRead(sessionId: string): Promise<void> {
|
|
await this.fetch(`/api/chat/sessions/${sessionId}/read`, { method: "POST" });
|
|
}
|
|
|
|
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> {
|
|
return this.fetch("/api/projects", {
|
|
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" });
|
|
}
|
|
|
|
// Project resources
|
|
async listProjectResources(
|
|
projectId: string,
|
|
): Promise<ListProjectResourcesResponse> {
|
|
return this.fetch(`/api/projects/${projectId}/resources`);
|
|
}
|
|
|
|
async createProjectResource(
|
|
projectId: string,
|
|
data: CreateProjectResourceRequest,
|
|
): Promise<ProjectResource> {
|
|
return this.fetch(`/api/projects/${projectId}/resources`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteProjectResource(
|
|
projectId: string,
|
|
resourceId: string,
|
|
): Promise<void> {
|
|
await this.fetch(`/api/projects/${projectId}/resources/${resourceId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// Labels
|
|
async listLabels(): Promise<ListLabelsResponse> {
|
|
return this.fetch(`/api/labels`);
|
|
}
|
|
|
|
async getLabel(id: string): Promise<Label> {
|
|
return this.fetch(`/api/labels/${id}`);
|
|
}
|
|
|
|
async createLabel(data: CreateLabelRequest): Promise<Label> {
|
|
return this.fetch(`/api/labels`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateLabel(id: string, data: UpdateLabelRequest): Promise<Label> {
|
|
return this.fetch(`/api/labels/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteLabel(id: string): Promise<void> {
|
|
await this.fetch(`/api/labels/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
async listLabelsForIssue(issueId: string): Promise<IssueLabelsResponse> {
|
|
return this.fetch(`/api/issues/${issueId}/labels`);
|
|
}
|
|
|
|
async attachLabel(issueId: string, labelId: string): Promise<IssueLabelsResponse> {
|
|
return this.fetch(`/api/issues/${issueId}/labels`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ label_id: labelId }),
|
|
});
|
|
}
|
|
|
|
async detachLabel(issueId: string, labelId: string): Promise<IssueLabelsResponse> {
|
|
return this.fetch(`/api/issues/${issueId}/labels/${labelId}`, {
|
|
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),
|
|
});
|
|
}
|
|
|
|
// Autopilots
|
|
async listAutopilots(params?: { status?: string }): Promise<ListAutopilotsResponse> {
|
|
const search = new URLSearchParams();
|
|
if (params?.status) search.set("status", params.status);
|
|
return this.fetch(`/api/autopilots?${search}`);
|
|
}
|
|
|
|
async getAutopilot(id: string): Promise<GetAutopilotResponse> {
|
|
return this.fetch(`/api/autopilots/${id}`);
|
|
}
|
|
|
|
async createAutopilot(data: CreateAutopilotRequest): Promise<Autopilot> {
|
|
return this.fetch("/api/autopilots", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateAutopilot(id: string, data: UpdateAutopilotRequest): Promise<Autopilot> {
|
|
return this.fetch(`/api/autopilots/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteAutopilot(id: string): Promise<void> {
|
|
await this.fetch(`/api/autopilots/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
async triggerAutopilot(id: string): Promise<AutopilotRun> {
|
|
return this.fetch(`/api/autopilots/${id}/trigger`, { method: "POST" });
|
|
}
|
|
|
|
async listAutopilotRuns(id: string, params?: { limit?: number; offset?: number }): Promise<ListAutopilotRunsResponse> {
|
|
const search = new URLSearchParams();
|
|
if (params?.limit) search.set("limit", params.limit.toString());
|
|
if (params?.offset) search.set("offset", params.offset.toString());
|
|
return this.fetch(`/api/autopilots/${id}/runs?${search}`);
|
|
}
|
|
|
|
async createAutopilotTrigger(autopilotId: string, data: CreateAutopilotTriggerRequest): Promise<AutopilotTrigger> {
|
|
return this.fetch(`/api/autopilots/${autopilotId}/triggers`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateAutopilotTrigger(autopilotId: string, triggerId: string, data: UpdateAutopilotTriggerRequest): Promise<AutopilotTrigger> {
|
|
return this.fetch(`/api/autopilots/${autopilotId}/triggers/${triggerId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteAutopilotTrigger(autopilotId: string, triggerId: string): Promise<void> {
|
|
await this.fetch(`/api/autopilots/${autopilotId}/triggers/${triggerId}`, { method: "DELETE" });
|
|
}
|
|
}
|