import { getApiKey, getBaseUrl } from "./config.js";
export class NexusAPIError extends Error {
constructor(
public status: number,
message: string
) {
super(message);
this.name = "NexusAPIError";
}
}
export interface ApiResponse<T> {
data?: T;
error?: string;
}
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const apiKey = getApiKey();
if (!apiKey) {
throw new NexusAPIError(401, "Not authenticated. Run: nexus-mcp login");
}
const baseUrl = getBaseUrl();
const url = `${baseUrl}/api/mcp${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
...options.headers,
},
});
if (!response.ok) {
const error = await response.text();
throw new NexusAPIError(response.status, error || response.statusText);
}
return response.json();
}
// ============================================================================
// Projects
// ============================================================================
export interface Project {
id: string;
name: string;
slug: string;
description: string;
url?: string;
status: string;
featureCompletion: number;
productionReadiness: number;
previewImage?: string;
testCredentials?: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
export async function listProjects(params?: {
status?: string;
assignedTo?: string;
limit?: number;
}): Promise<Project[]> {
const query = new URLSearchParams();
if (params?.status) query.set("status", params.status);
if (params?.assignedTo) query.set("assignedTo", params.assignedTo);
if (params?.limit) query.set("limit", params.limit.toString());
return request<Project[]>(`/projects?${query}`);
}
export async function getProject(idOrSlug: string): Promise<Project> {
return request<Project>(`/projects/${idOrSlug}`);
}
export async function createProject(data: {
name: string;
description?: string;
url?: string;
templateId?: string;
deadline?: string;
screenshotBase64?: string;
screenshotUrl?: string;
status?: string;
}): Promise<Project> {
return request<Project>("/projects", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateProject(
idOrSlug: string,
data: Partial<{
name: string;
description: string;
url: string;
status: string;
testCredentials: Record<string, unknown>;
setupData: Record<string, unknown>;
featureCompletion: number;
productionReadiness: number;
previewImage: string;
}>
): Promise<Project> {
return request<Project>(`/projects/${idOrSlug}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function updateProjectStatus(
idOrSlug: string,
status: string
): Promise<Project> {
return request<Project>(`/projects/${idOrSlug}/status`, {
method: "POST",
body: JSON.stringify({ status }),
});
}
// ============================================================================
// Bugs / Cards
// ============================================================================
export interface Card {
id: string;
projectId: string;
title: string;
description?: string;
type: "Task" | "Bug" | "Review" | "Doc";
status: string;
priority?: string;
labels: string[];
assigneeId?: string;
createdById: string;
createdAt: string;
updatedAt: string;
bug?: Bug;
}
export interface Bug {
id: string;
severity: string;
steps: string;
expected: string;
actual: string;
env?: string;
screenshotUrl?: string;
}
export async function listBugs(
projectIdOrSlug: string,
params?: {
status?: string;
severity?: string;
assignedToAI?: boolean;
limit?: number;
}
): Promise<Card[]> {
const query = new URLSearchParams();
if (params?.status) query.set("status", params.status);
if (params?.severity) query.set("severity", params.severity);
if (params?.assignedToAI !== undefined)
query.set("assignedToAI", params.assignedToAI.toString());
if (params?.limit) query.set("limit", params.limit.toString());
return request<Card[]>(`/projects/${projectIdOrSlug}/bugs?${query}`);
}
export async function createBug(
projectIdOrSlug: string,
data: {
title: string;
description: string;
severity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
steps?: string;
expected?: string;
actual?: string;
environment?: string;
screenshotUrl?: string;
priority?: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
labels?: string[];
assigneeId?: string;
}
): Promise<Card> {
return request<Card>(`/projects/${projectIdOrSlug}/bugs`, {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateCard(
cardId: string,
data: Partial<{
title: string;
description: string;
status: string;
priority: string;
labels: string[];
assigneeId: string;
}>
): Promise<Card> {
return request<Card>(`/cards/${cardId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function addComment(
cardId: string,
data: {
content: string;
mentions?: string[];
}
): Promise<{ id: string; content: string; createdAt: string }> {
return request(`/cards/${cardId}/comments`, {
method: "POST",
body: JSON.stringify(data),
});
}
// ============================================================================
// Templates
// ============================================================================
export interface Template {
id: string;
name: string;
slug: string;
description?: string;
kind: "UI" | "API" | "FULLSTACK";
githubRepo?: string;
previewImage?: string;
tags: string[];
}
export async function listTemplates(params?: {
kind?: string;
tags?: string[];
}): Promise<Template[]> {
const query = new URLSearchParams();
if (params?.kind) query.set("kind", params.kind);
if (params?.tags) query.set("tags", params.tags.join(","));
return request<Template[]>(`/templates?${query}`);
}
export async function createTemplate(data: {
name: string;
slug: string;
description?: string;
kind: "UI" | "API" | "FULLSTACK";
githubRepoUrl?: string;
previewImage?: string;
tags?: string[];
promptsJson?: Record<string, unknown>;
}): Promise<Template> {
return request<Template>("/templates", {
method: "POST",
body: JSON.stringify(data),
});
}
// ============================================================================
// Milestones
// ============================================================================
export interface MilestoneTask {
id: string;
title: string;
completed: boolean;
order: number;
}
export interface Milestone {
id: string;
title: string;
description?: string;
type: "GENERAL" | "ONBOARDING" | "LAUNCH" | "REVIEW" | "DEADLINE";
priority: "HIGH" | "MEDIUM" | "LOW";
date: string;
completed: boolean;
color?: string;
tasks: MilestoneTask[];
projects: { id: string; name: string }[];
}
export async function listMilestones(params?: {
startDate?: string;
endDate?: string;
type?: string;
completed?: boolean;
}): Promise<Milestone[]> {
const query = new URLSearchParams();
if (params?.startDate) query.set("startDate", params.startDate);
if (params?.endDate) query.set("endDate", params.endDate);
if (params?.type) query.set("type", params.type);
if (params?.completed !== undefined)
query.set("completed", params.completed.toString());
return request<Milestone[]>(`/milestones?${query}`);
}
export async function createMilestone(data: {
title: string;
date: string;
description?: string;
type?: string;
priority?: string;
color?: string;
projectIds?: string[];
tasks?: { title: string; order?: number }[];
businessEntity?: string;
paymentProvider?: string;
}): Promise<Milestone> {
return request<Milestone>("/milestones", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateMilestone(
id: string,
data: Partial<{
title: string;
description: string;
date: string;
completed: boolean;
addProjects: string[];
removeProjects: string[];
}>
): Promise<Milestone> {
return request<Milestone>(`/milestones/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function updateMilestoneTask(
milestoneId: string,
taskId: string,
data: Partial<{
title: string;
completed: boolean;
}>
): Promise<MilestoneTask> {
return request<MilestoneTask>(`/milestones/${milestoneId}/tasks/${taskId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
// ============================================================================
// Concepts
// ============================================================================
export interface Concept {
id: string;
name: string;
slug: string;
description: string;
status: string;
problemStatement?: string;
solution?: string;
businessModel?: string;
targetAudience?: string;
prdJson?: Record<string, unknown>;
templateId?: string;
projectId?: string;
createdAt: string;
}
export async function listConcepts(params?: {
status?: string;
templateId?: string;
}): Promise<Concept[]> {
const query = new URLSearchParams();
if (params?.status) query.set("status", params.status);
if (params?.templateId) query.set("templateId", params.templateId);
return request<Concept[]>(`/concepts?${query}`);
}
export async function getConcept(idOrSlug: string): Promise<Concept> {
return request<Concept>(`/concepts/${idOrSlug}`);
}
export async function createConcept(data: {
name: string;
description: string;
problemStatement?: string;
solution?: string;
businessModel?: string;
targetAudience?: string;
templateId?: string;
}): Promise<Concept> {
return request<Concept>("/concepts", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateConcept(
id: string,
data: Partial<{
name: string;
description: string;
status: string;
problemStatement: string;
solution: string;
prdJson: Record<string, unknown>;
}>
): Promise<Concept> {
return request<Concept>(`/concepts/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function promoteConcept(
id: string,
data?: { projectName?: string }
): Promise<{ project: Project }> {
return request(`/concepts/${id}/promote`, {
method: "POST",
body: JSON.stringify(data || {}),
});
}
// ============================================================================
// Auth
// ============================================================================
export async function verifyApiKey(): Promise<{
valid: boolean;
user?: { id: string; email: string; name: string };
org?: { id: string; name: string };
}> {
return request("/auth/verify");
}