import { eq, desc } from "drizzle-orm"; import { getSkillRegistryDb } from "./skill-registry-db.js"; import { personalRatings, agentSkills } from "./skill-registry-schema.js"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type RateOpts = { skillId: string; versionId?: string | null; stars: number; note?: string | null; }; type PersonalRatingRow = typeof personalRatings.$inferSelect; // --------------------------------------------------------------------------- // Factory // --------------------------------------------------------------------------- /** * Skill rating service factory. * Manages personal ratings and usage tracking in the skill registry libSQL DB. */ export function skillRatingService() { return { /** * Record a personal rating (1-5 stars) for a skill. * Always appends — never upserts — so rating history is preserved. */ async rate(opts: RateOpts): Promise { if (opts.stars < 1 || opts.stars > 5) { throw new RangeError(`stars must be between 1 and 5, got ${opts.stars}`); } const db = await getSkillRegistryDb(); const now = Date.now(); await db.insert(personalRatings).values({ id: crypto.randomUUID(), skillId: opts.skillId, versionId: opts.versionId ?? null, stars: opts.stars, note: opts.note ?? null, createdAt: now, updatedAt: now, }); }, /** * Get all personal ratings for a skill, ordered by createdAt descending (newest first). */ async getRatings(skillId: string): Promise { const db = await getSkillRegistryDb(); return db .select() .from(personalRatings) .where(eq(personalRatings.skillId, skillId)) .orderBy(desc(personalRatings.createdAt)); }, /** * Record a heartbeat run completion for all skills installed by an agent. * Increments task_count, updates running average cost, and sets last_used_at. * Safe to call when agent has no skills (no-op). * * @param agentId - the agent that just completed a successful run * @param costUsd - total cost of the run in USD, or null if unknown */ async recordUsageForAgent(agentId: string, costUsd: number | null): Promise { const db = await getSkillRegistryDb(); const client = db.$client as import("@libsql/client").Client; const now = Date.now(); if (costUsd !== null) { // Atomic update with running average calculation await client.execute({ sql: `UPDATE agent_skills SET task_count = task_count + 1, avg_cost_usd = CASE WHEN task_count = 0 THEN ? ELSE (COALESCE(avg_cost_usd, 0) * task_count + ?) / (task_count + 1) END, last_used_at = ? WHERE agent_id = ?`, args: [costUsd, costUsd, now, agentId], }); } else { // Skip avg_cost_usd update when cost is unknown await client.execute({ sql: `UPDATE agent_skills SET task_count = task_count + 1, last_used_at = ? WHERE agent_id = ?`, args: [now, agentId], }); } }, }; }