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"; 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()); // Globally installed skills live in ~/.agents/skills and are symlinked into // the agent's skills dir; isExistingFilePathAllowed resolves the symlink, so // the real target sits outside getAgentDir(). Allow the global skills root // too (the SDK always treats ~/.agents/skills as trusted). 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 }); } }