- Add personalRatings table to skill-registry-schema.ts - Add taskCount, avgCostUsd, lastUsedAt columns to agentSkills in schema - Add CREATE_PERSONAL_RATINGS_TABLE DDL constant in skill-registry-db.ts - Add ALTER TABLE statements for new agent_skills usage columns (idempotent) - Create skill-registry-ratings.ts with skillRatingService factory - rate() appends personal rating, validates stars 1-5 - getRatings() returns ratings ordered by createdAt DESC - recordUsageForAgent() atomically updates task_count, avg_cost_usd, last_used_at - All 8 tests pass
186 lines
5.1 KiB
TypeScript
186 lines
5.1 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
|
import { dirname } from "node:path";
|
|
import { drizzle } from "drizzle-orm/libsql";
|
|
import { createClient, type Client as LibSQLClient } from "@libsql/client";
|
|
import * as schema from "./skill-registry-schema.js";
|
|
import { resolveSkillRegistryDbPath } from "../home-paths.js";
|
|
|
|
export type SkillRegistryDb = ReturnType<typeof drizzle<typeof schema>>;
|
|
|
|
let _db: SkillRegistryDb | null = null;
|
|
|
|
const CREATE_SKILLS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skills (
|
|
id TEXT PRIMARY KEY,
|
|
source_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
source_url TEXT,
|
|
active_version_id TEXT,
|
|
removed_at INTEGER,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
)`;
|
|
|
|
const CREATE_SKILL_VERSIONS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skill_versions (
|
|
id TEXT PRIMARY KEY,
|
|
skill_id TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
fetched_at INTEGER NOT NULL,
|
|
cache_dir TEXT
|
|
)`;
|
|
|
|
const CREATE_SKILL_FILES_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skill_files (
|
|
id TEXT PRIMARY KEY,
|
|
version_id TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
size_bytes INTEGER
|
|
)`;
|
|
|
|
const CREATE_COMMUNITY_RATINGS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS community_ratings (
|
|
id TEXT PRIMARY KEY,
|
|
skill_id TEXT NOT NULL,
|
|
fetched_at INTEGER NOT NULL,
|
|
average_rating REAL,
|
|
rating_count INTEGER,
|
|
source TEXT
|
|
)`;
|
|
|
|
const CREATE_SKILL_GROUPS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skill_groups (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
)`;
|
|
|
|
const CREATE_SKILL_GROUP_MEMBERS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skill_group_members (
|
|
group_id TEXT NOT NULL,
|
|
skill_id TEXT NOT NULL,
|
|
added_at INTEGER NOT NULL,
|
|
PRIMARY KEY (group_id, skill_id)
|
|
)`;
|
|
|
|
const CREATE_SKILL_GROUP_INHERITANCE_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS skill_group_inheritance (
|
|
child_group_id TEXT NOT NULL,
|
|
parent_group_id TEXT NOT NULL,
|
|
PRIMARY KEY (child_group_id, parent_group_id)
|
|
)`;
|
|
|
|
const CREATE_AGENT_SKILL_GROUPS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS agent_skill_groups (
|
|
agent_id TEXT NOT NULL,
|
|
group_id TEXT NOT NULL,
|
|
assigned_at INTEGER NOT NULL,
|
|
PRIMARY KEY (agent_id, group_id)
|
|
)`;
|
|
|
|
const CREATE_AGENT_SKILLS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
agent_id TEXT NOT NULL,
|
|
skill_id TEXT NOT NULL,
|
|
installed_at INTEGER NOT NULL,
|
|
PRIMARY KEY (agent_id, skill_id)
|
|
)`;
|
|
|
|
const CREATE_PERSONAL_RATINGS_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS personal_ratings (
|
|
id TEXT PRIMARY KEY,
|
|
skill_id TEXT NOT NULL,
|
|
version_id TEXT,
|
|
stars INTEGER NOT NULL,
|
|
note TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
)`;
|
|
|
|
const BUILTIN_GROUPS = [
|
|
{
|
|
id: "builtin/pm-essentials",
|
|
name: "PM Essentials",
|
|
description: "Core planning and project-management skills",
|
|
},
|
|
{
|
|
id: "builtin/engineer-core",
|
|
name: "Engineer Core",
|
|
description: "Foundational engineering skills",
|
|
},
|
|
{
|
|
id: "builtin/frontend",
|
|
name: "Frontend",
|
|
description: "UI and frontend development skills",
|
|
},
|
|
{
|
|
id: "builtin/backend",
|
|
name: "Backend",
|
|
description: "API, database, and infrastructure skills",
|
|
},
|
|
{
|
|
id: "builtin/creative",
|
|
name: "Creative",
|
|
description: "Writing, branding, and creative production",
|
|
},
|
|
] as const;
|
|
|
|
async function seedBuiltinGroups(client: LibSQLClient): Promise<void> {
|
|
const now = Date.now();
|
|
for (const group of BUILTIN_GROUPS) {
|
|
await client.execute({
|
|
sql: `INSERT OR IGNORE INTO skill_groups (id, name, description, is_builtin, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)`,
|
|
args: [group.id, group.name, group.description, now, now],
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
|
|
if (_db !== null) return _db;
|
|
|
|
const dbPath = resolveSkillRegistryDbPath();
|
|
await mkdir(dirname(dbPath), { recursive: true });
|
|
|
|
const client = createClient({ url: `file:${dbPath}` });
|
|
_db = drizzle({ client, schema });
|
|
|
|
await client.execute(CREATE_SKILLS_TABLE);
|
|
await client.execute(CREATE_SKILL_VERSIONS_TABLE);
|
|
await client.execute(CREATE_SKILL_FILES_TABLE);
|
|
await client.execute(CREATE_COMMUNITY_RATINGS_TABLE);
|
|
|
|
await client.execute(CREATE_SKILL_GROUPS_TABLE);
|
|
await client.execute(CREATE_SKILL_GROUP_MEMBERS_TABLE);
|
|
await client.execute(CREATE_SKILL_GROUP_INHERITANCE_TABLE);
|
|
await client.execute(CREATE_AGENT_SKILL_GROUPS_TABLE);
|
|
await client.execute(CREATE_AGENT_SKILLS_TABLE);
|
|
await client.execute(CREATE_PERSONAL_RATINGS_TABLE);
|
|
|
|
// Add usage-tracking columns to agent_skills if they don't exist yet (idempotent)
|
|
const agentSkillsAlters = [
|
|
`ALTER TABLE agent_skills ADD COLUMN task_count INTEGER NOT NULL DEFAULT 0`,
|
|
`ALTER TABLE agent_skills ADD COLUMN avg_cost_usd REAL`,
|
|
`ALTER TABLE agent_skills ADD COLUMN last_used_at INTEGER`,
|
|
];
|
|
for (const sql of agentSkillsAlters) {
|
|
try {
|
|
await client.execute(sql);
|
|
} catch {
|
|
// Column already exists — ignore
|
|
}
|
|
}
|
|
|
|
await seedBuiltinGroups(client);
|
|
|
|
return _db;
|
|
}
|
|
|
|
/** Reset the singleton — used for test cleanup */
|
|
export function resetSkillRegistryDb(): void {
|
|
_db = null;
|
|
}
|