Files
pi-agent-integrated/pi-web/app/api/skills/route.ts
T

78 lines
3.4 KiB
TypeScript

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=<path>
// 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<Record<string, unknown>>(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 });
}
}