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
146 changes: 146 additions & 0 deletions data/studio-sessions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
[
{
"id": "studio_1001",
"skillSlug": "content-summarizer",
"skillName": "Content Summarizer",
"status": "completed",
"inputs": {
"source": "https://foragents.dev/docs/bootstrap",
"tone": "technical",
"maxBullets": 5
},
"outputs": {
"summary": "Bootstrap docs define setup flow, required files, and validation checks.",
"bullets": [
"Load bootstrap files first",
"Validate environment assumptions",
"Track run metadata"
]
},
"logs": [
{
"timestamp": "2026-02-09T19:00:12.000Z",
"message": "Session queued"
},
{
"timestamp": "2026-02-09T19:00:14.000Z",
"message": "Fetched source content"
},
{
"timestamp": "2026-02-09T19:00:20.000Z",
"message": "Generated summary output"
}
],
"startedAt": "2026-02-09T19:00:12.000Z",
"completedAt": "2026-02-09T19:00:21.000Z"
},
{
"id": "studio_1002",
"skillSlug": "dependency-scanner",
"skillName": "Dependency Scanner",
"status": "active",
"inputs": {
"repo": "reflectt/foragents.dev",
"depth": "full"
},
"outputs": {
"checkedPackages": 112,
"vulnerabilities": 0
},
"logs": [
{
"timestamp": "2026-02-09T21:10:02.000Z",
"message": "Scanning lockfile"
},
{
"timestamp": "2026-02-09T21:10:18.000Z",
"message": "Resolving transitive dependency graph"
}
],
"startedAt": "2026-02-09T21:10:02.000Z",
"completedAt": null
},
{
"id": "studio_1003",
"skillSlug": "release-note-writer",
"skillName": "Release Note Writer",
"status": "error",
"inputs": {
"range": "v0.9.0..v1.0.0",
"includeBreaking": true
},
"outputs": {
"error": "No git tag found for v0.9.0"
},
"logs": [
{
"timestamp": "2026-02-08T15:42:00.000Z",
"message": "Collecting commits"
},
{
"timestamp": "2026-02-08T15:42:03.000Z",
"message": "Failed to resolve start tag v0.9.0"
}
],
"startedAt": "2026-02-08T15:42:00.000Z",
"completedAt": "2026-02-08T15:42:03.000Z"
},
{
"id": "studio_1004",
"skillSlug": "api-schema-checker",
"skillName": "API Schema Checker",
"status": "completed",
"inputs": {
"endpoint": "/api/skill/[slug]",
"strict": false
},
"outputs": {
"warnings": [
"Missing example for 404 response"
],
"status": "pass"
},
"logs": [
{
"timestamp": "2026-02-07T12:07:10.000Z",
"message": "Loaded route schema"
},
{
"timestamp": "2026-02-07T12:07:13.000Z",
"message": "Validated request and response shapes"
}
],
"startedAt": "2026-02-07T12:07:10.000Z",
"completedAt": "2026-02-07T12:07:14.000Z"
},
{
"id": "studio_1005",
"skillSlug": "log-triage",
"skillName": "Log Triage",
"status": "completed",
"inputs": {
"window": "24h",
"severity": "warn"
},
"outputs": {
"clusters": 3,
"topIssue": "Missing auth header on /api/verify/start"
},
"logs": [
{
"timestamp": "2026-02-06T08:33:44.000Z",
"message": "Indexed 1,932 log lines"
},
{
"timestamp": "2026-02-06T08:34:01.000Z",
"message": "Ranked anomaly clusters"
},
{
"timestamp": "2026-02-06T08:34:09.000Z",
"message": "Exported triage report"
}
],
"startedAt": "2026-02-06T08:33:44.000Z",
"completedAt": "2026-02-06T08:34:09.000Z"
}
]
103 changes: 103 additions & 0 deletions src/app/api/studio/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import {
isStudioSessionStatus,
readStudioSessions,
sanitizeOutputs,
StudioSession,
writeStudioSessions,
} from "@/lib/studioStore";

type UpdateStudioSessionRequest = {
status?: unknown;
outputs?: unknown;
logMessage?: unknown;
};

export async function GET(
_request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id } = await context.params;
const sessions = await readStudioSessions();
const session = sessions.find((item) => item.id === id);

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

return NextResponse.json(
{
session,
updatedAt: new Date().toISOString(),
},
{
headers: {
"Cache-Control": "no-store",
},
}
);
}

export async function PATCH(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id } = await context.params;

let payload: UpdateStudioSessionRequest;
try {
payload = (await request.json()) as UpdateStudioSessionRequest;
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const sessions = await readStudioSessions();
const targetIndex = sessions.findIndex((session) => session.id === id);

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

const current = sessions[targetIndex] as StudioSession;
const now = new Date().toISOString();

const nextStatus = payload.status;
if (typeof nextStatus !== "undefined" && !isStudioSessionStatus(nextStatus)) {
return NextResponse.json({ error: "status must be active, completed, or error" }, { status: 400 });
}

const logMessage = typeof payload.logMessage === "string" ? payload.logMessage.trim() : "";

const updatedSession: StudioSession = {
...current,
status: isStudioSessionStatus(nextStatus) ? nextStatus : current.status,
outputs:
typeof payload.outputs === "undefined"
? current.outputs
: {
...current.outputs,
...sanitizeOutputs(payload.outputs),
},
logs: logMessage
? [
...current.logs,
{
timestamp: now,
message: logMessage,
},
]
: current.logs,
completedAt:
isStudioSessionStatus(nextStatus) && nextStatus !== "active"
? now
: isStudioSessionStatus(nextStatus) && nextStatus === "active"
? null
: current.completedAt,
};

const nextSessions = [...sessions];
nextSessions[targetIndex] = updatedSession;
await writeStudioSessions(nextSessions);

return NextResponse.json({ session: updatedSession });
}
82 changes: 82 additions & 0 deletions src/app/api/studio/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import {
formatSkillNameFromSlug,
readStudioSessions,
sanitizeInputs,
StudioSession,
writeStudioSessions,
} from "@/lib/studioStore";

type StartStudioSessionRequest = {
skillSlug?: unknown;
skillName?: unknown;
inputs?: unknown;
};

export async function GET() {
const sessions = await readStudioSessions();

return NextResponse.json(
{
sessions,
count: sessions.length,
updatedAt: new Date().toISOString(),
},
{
headers: {
"Cache-Control": "no-store",
},
}
);
}

export async function POST(request: NextRequest) {
let payload: StartStudioSessionRequest;

try {
payload = (await request.json()) as StartStudioSessionRequest;
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const skillSlug = typeof payload.skillSlug === "string" ? payload.skillSlug.trim().toLowerCase() : "";

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

const skillName =
typeof payload.skillName === "string" && payload.skillName.trim()
? payload.skillName.trim()
: formatSkillNameFromSlug(skillSlug);

const inputs = sanitizeInputs(payload.inputs);
const sessions = await readStudioSessions();
const now = new Date().toISOString();

const nextSession: StudioSession = {
id: `studio_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
skillSlug,
skillName,
status: "active",
inputs,
outputs: {},
logs: [
{
timestamp: now,
message: `Started ${skillName}`,
},
],
startedAt: now,
completedAt: null,
};

await writeStudioSessions([nextSession, ...sessions]);

return NextResponse.json(
{
session: nextSession,
},
{ status: 201 }
);
}
Loading
Loading