Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions data/builder-drafts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[
{
"id": "draft_skill_review_assistant",
"name": "Skill Review Assistant",
"slug": "skill-review-assistant",
"description": "Reviews skill definitions for clarity, safety, and production readiness.",
"version": "0.1.0",
"author": "Team Reflectt",
"tags": ["quality", "safety", "review"],
"files": [
{
"name": "SKILL.md",
"content": "# Skill Review Assistant\n\nYou analyze new skills before publication.\n\n## Checklist\n- Validate required fields\n- Flag ambiguous instructions\n- Recommend test prompts\n"
},
{
"name": "tests/prompts.md",
"content": "# Prompt Tests\n\n1. Ask the agent to summarize a risky command.\n2. Verify it requests confirmation before destructive actions.\n"
}
],
"createdAt": "2026-02-01T09:00:00.000Z",
"updatedAt": "2026-02-03T11:15:00.000Z",
"status": "draft"
},
{
"id": "draft_doc_string_polisher",
"name": "Docstring Polisher",
"slug": "docstring-polisher",
"description": "Improves inline docs and API examples while preserving technical accuracy.",
"version": "1.2.0",
"author": "Kai",
"tags": ["documentation", "developer-experience"],
"files": [
{
"name": "README.md",
"content": "# Docstring Polisher\n\nTurns rough function comments into concise, consistent docs.\n"
},
{
"name": "templates/python.md",
"content": "## Python Template\n\nArgs:\nReturns:\nRaises:\nExamples:\n"
}
],
"createdAt": "2026-01-21T16:40:00.000Z",
"updatedAt": "2026-02-05T14:05:00.000Z",
"status": "published"
},
{
"id": "draft_web_research_pack",
"name": "Web Research Pack",
"slug": "web-research-pack",
"description": "Collects, compares, and summarizes sources with citation links for quick decision-making.",
"version": "0.4.3",
"author": "Reflectt Community",
"tags": ["research", "web", "citations"],
"files": [
{
"name": "SKILL.md",
"content": "# Web Research Pack\n\nGather 3-5 relevant sources and summarize key differences.\n"
},
{
"name": "output/schema.json",
"content": "{\n \"summary\": \"string\",\n \"sources\": [\n {\n \"title\": \"string\",\n \"url\": \"string\",\n \"note\": \"string\"\n }\n ]\n}\n"
}
],
"createdAt": "2026-02-06T08:10:00.000Z",
"updatedAt": "2026-02-07T18:22:00.000Z",
"status": "draft"
}
]
111 changes: 111 additions & 0 deletions src/app/api/builder/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from "next/server";
import { BuilderDraft, readBuilderDrafts, slugify, writeBuilderDrafts } from "@/lib/builderStore";

type RouteParams = {
params: Promise<{ id: string }>;
};

type PatchPayload = {
name?: unknown;
description?: unknown;
version?: unknown;
author?: unknown;
tags?: unknown;
files?: unknown;
status?: unknown;
};

function normalizeTags(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
return value
.filter((tag): tag is string => typeof tag === "string")
.map((tag) => tag.trim().toLowerCase())
.filter(Boolean);
}

function normalizeFiles(value: unknown): BuilderDraft["files"] | undefined {
if (!Array.isArray(value)) return undefined;

return value
.map((file) => {
if (!file || typeof file !== "object") return null;

const name = typeof (file as { name?: unknown }).name === "string"
? (file as { name: string }).name.trim()
: "";

if (!name) return null;

return {
name,
content: typeof (file as { content?: unknown }).content === "string"
? (file as { content: string }).content
: "",
};
})
.filter((file): file is { name: string; content: string } => file !== null);
}

export async function GET(_request: NextRequest, context: RouteParams) {
const { id } = await context.params;
const drafts = await readBuilderDrafts();
const draft = drafts.find((item) => item.id === id);

if (!draft) {
return NextResponse.json({ error: "Draft not found." }, { status: 404 });
}

return NextResponse.json({ draft }, { headers: { "Cache-Control": "no-store" } });
}

export async function PATCH(request: NextRequest, context: RouteParams) {
try {
const { id } = await context.params;
const body = (await request.json()) as PatchPayload;

const drafts = await readBuilderDrafts();
const index = drafts.findIndex((item) => item.id === id);

if (index === -1) {
return NextResponse.json({ error: "Draft not found." }, { status: 404 });
}

const current = drafts[index];
const nextName = typeof body.name === "string" ? body.name.trim() : current.name;

const updated: BuilderDraft = {
...current,
name: nextName,
slug: nextName !== current.name ? slugify(nextName) || current.slug : current.slug,
description: typeof body.description === "string" ? body.description.trim() : current.description,
version: typeof body.version === "string" && body.version.trim() ? body.version.trim() : current.version,
author: typeof body.author === "string" && body.author.trim() ? body.author.trim() : current.author,
tags: normalizeTags(body.tags) ?? current.tags,
files: normalizeFiles(body.files) ?? current.files,
status: body.status === "published" || body.status === "draft" ? body.status : current.status,
updatedAt: new Date().toISOString(),
};

drafts[index] = updated;
await writeBuilderDrafts(drafts);

return NextResponse.json({ success: true, draft: updated });
} catch {
return NextResponse.json({ error: "Invalid request body. Expected JSON." }, { status: 400 });
}
}

export async function DELETE(_request: NextRequest, context: RouteParams) {
const { id } = await context.params;
const drafts = await readBuilderDrafts();
const index = drafts.findIndex((item) => item.id === id);

if (index === -1) {
return NextResponse.json({ error: "Draft not found." }, { status: 404 });
}

const [deleted] = drafts.splice(index, 1);
await writeBuilderDrafts(drafts);

return NextResponse.json({ success: true, deleted });
}
107 changes: 107 additions & 0 deletions src/app/api/builder/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from "next/server";
import { BuilderDraft, readBuilderDrafts, slugify, writeBuilderDrafts } from "@/lib/builderStore";

function makeId(name: string): string {
const base = slugify(name) || "skill";
return `${base}_${Date.now().toString(36)}`;
}

function parsePayload(body: unknown): Omit<BuilderDraft, "id" | "slug" | "createdAt" | "updatedAt"> | null {
if (!body || typeof body !== "object") return null;

const payload = body as {
name?: unknown;
description?: unknown;
version?: unknown;
author?: unknown;
tags?: unknown;
files?: unknown;
status?: unknown;
};

const name = typeof payload.name === "string" ? payload.name.trim() : "";
if (!name) return null;

const tags = Array.isArray(payload.tags)
? payload.tags
.filter((tag): tag is string => typeof tag === "string")
.map((tag) => tag.trim().toLowerCase())
.filter(Boolean)
: [];

const files = Array.isArray(payload.files)
? payload.files
.map((file) => {
if (!file || typeof file !== "object") return null;

const fileName = typeof (file as { name?: unknown }).name === "string"
? (file as { name: string }).name.trim()
: "";

if (!fileName) return null;

return {
name: fileName,
content: typeof (file as { content?: unknown }).content === "string"
? (file as { content: string }).content
: "",
};
})
.filter((file): file is { name: string; content: string } => file !== null)
: [];

return {
name,
description: typeof payload.description === "string" ? payload.description.trim() : "",
version: typeof payload.version === "string" && payload.version.trim() ? payload.version.trim() : "0.1.0",
author: typeof payload.author === "string" && payload.author.trim() ? payload.author.trim() : "Unknown",
tags,
files,
status: payload.status === "published" ? "published" : "draft",
};
}

export async function GET() {
const drafts = await readBuilderDrafts();
const sorted = [...drafts].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));

return NextResponse.json(
{
drafts: sorted,
count: sorted.length,
},
{
headers: {
"Cache-Control": "no-store",
},
}
);
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as unknown;
const payload = parsePayload(body);

if (!payload) {
return NextResponse.json({ error: "Name is required." }, { status: 400 });
}

const now = new Date().toISOString();
const draft: BuilderDraft = {
...payload,
id: makeId(payload.name),
slug: slugify(payload.name) || makeId(payload.name),
createdAt: now,
updatedAt: now,
};

const drafts = await readBuilderDrafts();
drafts.unshift(draft);
await writeBuilderDrafts(drafts);

return NextResponse.json({ success: true, draft }, { status: 201 });
} catch {
return NextResponse.json({ error: "Invalid request body. Expected JSON." }, { status: 400 });
}
}
Loading
Loading