- Add POST/GET /skill-registry/skills/:sourceId/:slug/ratings routes - Import skillRatingService in skill-registry routes - Add upsertCommunityRatingsStub() in fetcher, called after each skill upsert - Import communityRatings from schema in fetcher - Update list() and getById() in skill-registry.ts to LEFT JOIN communityRatings - Include averageRating, ratingCount, taskCount, avgCostUsd, lastUsedAt in SkillListItem - Add agentSkills usage aggregation via LEFT JOIN + SUM/AVG/MAX - Add fire-and-forget recordUsageForAgent call in heartbeat after finalizeAgentStatus - Dynamic import keeps skill-registry-ratings off critical startup path - All 44 skill-registry tests pass, full server suite (536) green
102 lines
4 KiB
TypeScript
102 lines
4 KiB
TypeScript
import { Router } from "express";
|
|
import { skillRegistryService } from "../services/skill-registry.js";
|
|
import { skillRatingService } from "../services/skill-registry-ratings.js";
|
|
import { assertBoard } from "./authz.js";
|
|
|
|
/**
|
|
* REST routes for the skill registry.
|
|
*
|
|
* Note: does NOT take a db param — the skill registry manages its own libSQL database.
|
|
* All route handlers assert `board` access before delegating to skillRegistryService.
|
|
*/
|
|
export function skillRegistryRoutes(): Router {
|
|
const router = Router();
|
|
const svc = skillRegistryService();
|
|
|
|
// List all skills (soft-deleted excluded by default)
|
|
router.get("/skill-registry/skills", async (req, res) => {
|
|
assertBoard(req);
|
|
const includeRemoved = req.query.includeRemoved === "true";
|
|
const list = await svc.list({ includeRemoved });
|
|
res.json(list);
|
|
});
|
|
|
|
// Get versions for a skill — must be registered before the single-skill route
|
|
// to avoid /:id matching "versions" as the id segment
|
|
router.get("/skill-registry/skills/:sourceId/:slug/versions", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const versions = await svc.getVersions(skillId);
|
|
res.json(versions);
|
|
});
|
|
|
|
// Install skill to agent directory
|
|
router.post("/skill-registry/skills/:sourceId/:slug/install", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const { agentSkillsDir } = req.body as { agentSkillsDir: string };
|
|
if (!agentSkillsDir) return res.status(400).json({ error: "agentSkillsDir required" });
|
|
const result = await svc.install(skillId, agentSkillsDir);
|
|
res.json(result);
|
|
});
|
|
|
|
// Rollback to a specific version
|
|
router.post("/skill-registry/skills/:sourceId/:slug/rollback", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const { versionId, agentSkillsDir } = req.body as { versionId: string; agentSkillsDir: string };
|
|
if (!versionId || !agentSkillsDir) {
|
|
return res.status(400).json({ error: "versionId and agentSkillsDir required" });
|
|
}
|
|
await svc.rollback(skillId, versionId, agentSkillsDir);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Soft-delete a skill
|
|
router.delete("/skill-registry/skills/:sourceId/:slug", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
await svc.uninstall(skillId);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Submit a personal rating for a skill
|
|
router.post("/skill-registry/skills/:sourceId/:slug/ratings", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const { stars, versionId, note } = req.body as { stars: number; versionId?: string; note?: string };
|
|
if (typeof stars !== "number" || stars < 1 || stars > 5) {
|
|
return res.status(400).json({ error: "stars must be a number between 1 and 5" });
|
|
}
|
|
const ratingSvc = skillRatingService();
|
|
await ratingSvc.rate({ skillId, versionId: versionId ?? null, stars, note: note ?? null });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Get personal ratings for a skill
|
|
router.get("/skill-registry/skills/:sourceId/:slug/ratings", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const ratingSvc = skillRatingService();
|
|
const ratings = await ratingSvc.getRatings(skillId);
|
|
res.json(ratings);
|
|
});
|
|
|
|
// Get a single skill by id
|
|
router.get("/skill-registry/skills/:sourceId/:slug", async (req, res) => {
|
|
assertBoard(req);
|
|
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
|
const skill = await svc.getById(skillId);
|
|
if (!skill) return res.status(404).json({ error: "Skill not found" });
|
|
res.json(skill);
|
|
});
|
|
|
|
// Trigger fetch from all configured sources
|
|
router.post("/skill-registry/fetch", async (req, res) => {
|
|
assertBoard(req);
|
|
const result = await svc.fetchAll();
|
|
res.json(result);
|
|
});
|
|
|
|
return router;
|
|
}
|