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>; 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 { 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 { 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`, `ALTER TABLE agent_skills ADD COLUMN source TEXT NOT NULL DEFAULT 'managed'`, ]; 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; }