import { Router } from "express"; import os from "node:os"; import path from "node:path"; import { skillGroupService } from "../services/skill-registry-groups.js"; import { assertBoard } from "./authz.js"; /** Default skills directory when client doesn't provide one */ function defaultSkillsDir(): string { return path.join(os.homedir(), ".claude", "skills"); } /** * REST routes for skill groups. * * Note: does NOT take a db param — skill groups use the libSQL registry.db. * All route handlers assert `board` access before delegating to skillGroupService. */ export function skillGroupRoutes(): Router { const router = Router(); const svc = skillGroupService(); function handleError(res: any, err: unknown) { const msg = err instanceof Error ? err.message : String(err); if ( msg.includes("Cannot delete built-in") || msg.includes("not found") || msg.includes("cycle") || msg.includes("required") ) { return res.status(400).json({ error: msg }); } if (msg.includes("already exists")) { return res.status(409).json({ error: msg }); } return res.status(500).json({ error: msg }); } // --- Group CRUD --- router.get("/skill-registry/groups", async (req, res) => { assertBoard(req); try { const groups = await svc.listGroups(); res.json(groups); } catch (err) { handleError(res, err); } }); // Import route must come BEFORE /:groupId to avoid "import" being captured as a groupId param router.post("/skill-registry/groups/import", async (req, res) => { assertBoard(req); try { const result = await svc.importGroup(req.body); res.status(201).json(result); } catch (err) { handleError(res, err); } }); router.post("/skill-registry/groups", async (req, res) => { assertBoard(req); try { const { name, description } = req.body as { name?: string; description?: string }; if (!name) { return res.status(400).json({ error: "name required" }); } const group = await svc.createGroup({ name, description }); res.status(201).json(group); } catch (err) { handleError(res, err); } }); router.get("/skill-registry/groups/:groupId", async (req, res) => { assertBoard(req); try { const group = await svc.getGroup(req.params.groupId); if (!group) { return res.status(404).json({ error: "Group not found" }); } res.json(group); } catch (err) { handleError(res, err); } }); router.patch("/skill-registry/groups/:groupId", async (req, res) => { assertBoard(req); try { const { name, description } = req.body as { name?: string; description?: string }; const group = await svc.updateGroup(req.params.groupId, { name, description }); res.json(group); } catch (err) { handleError(res, err); } }); router.delete("/skill-registry/groups/:groupId", async (req, res) => { assertBoard(req); try { await svc.deleteGroup(req.params.groupId); res.status(204).end(); } catch (err) { handleError(res, err); } }); // --- Members --- router.get("/skill-registry/groups/:groupId/members", async (req, res) => { assertBoard(req); try { const members = await svc.listMembers(req.params.groupId); res.json(members); } catch (err) { handleError(res, err); } }); router.post("/skill-registry/groups/:groupId/members", async (req, res) => { assertBoard(req); try { const { skillId } = req.body as { skillId?: string }; if (!skillId) { return res.status(400).json({ error: "skillId required" }); } await svc.addMember(req.params.groupId, skillId); res.status(201).json({ ok: true }); } catch (err) { handleError(res, err); } }); router.delete("/skill-registry/groups/:groupId/members/:skillId(*)", async (req, res) => { assertBoard(req); try { const skillId = req.params.skillId; await svc.removeMember(req.params.groupId, skillId); res.status(204).end(); } catch (err) { handleError(res, err); } }); // --- Export --- router.get("/skill-registry/groups/:groupId/export", async (req, res) => { assertBoard(req); try { const data = await svc.exportGroup(req.params.groupId); res.setHeader("Content-Disposition", `attachment; filename="${data.group.name}.json"`); res.json(data); } catch (err) { handleError(res, err); } }); // --- Agent group assignments --- router.get("/skill-registry/agents/:agentId/groups", async (req, res) => { assertBoard(req); try { const groups = await svc.listAgentGroups(req.params.agentId); res.json(groups); } catch (err) { handleError(res, err); } }); router.post("/skill-registry/agents/:agentId/groups", async (req, res) => { assertBoard(req); try { const { groupId, agentSkillsDir } = req.body as { groupId?: string; agentSkillsDir?: string }; if (!groupId) { return res.status(400).json({ error: "groupId required" }); } const resolvedDir = agentSkillsDir || defaultSkillsDir(); const result = await svc.assignGroup(groupId, req.params.agentId, resolvedDir); res.status(201).json(result); } catch (err) { handleError(res, err); } }); router.delete("/skill-registry/agents/:agentId/groups/:groupId(*)", async (req, res) => { assertBoard(req); try { const { agentSkillsDir } = req.body as { agentSkillsDir?: string }; const resolvedDir = agentSkillsDir || defaultSkillsDir(); await svc.removeGroup(req.params.groupId, req.params.agentId, resolvedDir); res.status(204).end(); } catch (err) { handleError(res, err); } }); router.get("/skill-registry/agents/:agentId/skills", async (req, res) => { assertBoard(req); try { const skills = await svc.listAgentSkills(req.params.agentId); res.json(skills); } catch (err) { handleError(res, err); } }); return router; }