import { NextResponse } from "next/server"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { homedir } from "os"; import path from "path"; import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; import { loadSkillsWithInstallInfo } from "@/lib/skills-service"; import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access"; import { getManagedRuntimePaths, isManagedRuntime } from "@/lib/app-runtime"; export const dynamic = "force-dynamic"; // GET /api/skills?cwd= // Uses DefaultResourceLoader (same logic as AgentSession startup) so settings.json // skill paths, package skills, and .agents/skills directories are all included. export async function GET(req: Request) { const { searchParams } = new URL(req.url); const cwd = searchParams.get("cwd"); if (!cwd) return NextResponse.json({ error: "cwd required" }, { status: 400 }); try { const allowedRoots = await getAllowedFileRoots(); if (!isExistingFilePathAllowed(cwd, allowedRoots)) { return NextResponse.json({ error: "Access denied" }, { status: 403 }); } return NextResponse.json(await loadSkillsWithInstallInfo(cwd)); } catch (e) { return NextResponse.json({ error: String(e) }, { status: 500 }); } } // PATCH /api/skills — toggle disable-model-invocation on a SKILL.md file export async function PATCH(req: Request) { try { const body = await req.json() as { filePath: string; disableModelInvocation: boolean }; const { filePath, disableModelInvocation } = body; if (!filePath) return NextResponse.json({ error: "filePath required" }, { status: 400 }); if (!existsSync(filePath)) return NextResponse.json({ error: "file not found" }, { status: 404 }); const allowedRoots = new Set(await getAllowedFileRoots()); allowedRoots.add(getAgentDir()); if (isManagedRuntime()) { for (const root of getManagedRuntimePaths().managedSkillRoots) { if (existsSync(root)) allowedRoots.add(root); } } else { // Upstream-compatible mode keeps the CLI's user-wide skill root. const globalSkillsDir = path.join(homedir(), ".agents", "skills"); if (existsSync(globalSkillsDir)) allowedRoots.add(globalSkillsDir); } if (!isExistingFilePathAllowed(filePath, allowedRoots)) { return NextResponse.json({ error: "Access denied" }, { status: 403 }); } const content = readFileSync(filePath, "utf8"); const key = "disable-model-invocation"; // Use parseFrontmatter to check current value, then do a surgical line edit // to preserve the original YAML formatting of all other fields. const { frontmatter } = parseFrontmatter>(content); const alreadySet = Boolean(frontmatter[key]); let updated = content; if (disableModelInvocation && !alreadySet) { // Add key after the opening --- line updated = content.replace(/^---\r?\n/, `---\n${key}: true\n`); // If no frontmatter exists, create one if (updated === content) updated = `---\n${key}: true\n---\n${content}`; } else if (!disableModelInvocation && alreadySet) { // Remove the key line entirely updated = content.replace(new RegExp(`^${key}\\s*:.*\\r?\\n`, "m"), ""); } writeFileSync(filePath, updated, "utf8"); return NextResponse.json({ success: true }); } catch (e) { return NextResponse.json({ error: String(e) }, { status: 500 }); } }