import { packageStorage } from 'kody:runtime' /** * Always resolve the registry bucket through `packageStorage()`: it is bound * to THIS package's bucket in every context — the package's own runtime, and * statically imported copies of this code running in ad hoc execute or other * packages. Ambient `storage` would silently point at the caller's bucket * (or nothing) in those foreign contexts. Called per-use, never cached at * module scope, to respect the runtime's late-binding contract. */ function bucket() { return packageStorage() } let schemaReady = false export type SkillRow = { id: string name: string description: string created_at: string updated_at: string } export type SkillFileRow = { skill_id: string path: string content: string updated_at: string } export type SkillVersionRow = { id: number skill_id: string path: string content: string replaced_at: string } export type SkillRevisionRow = { id: number commit_oid: string skill_id: string path: string replaced_at: string content_length: number } export type SkillsIndexRow = { id: string name: string description: string files_json: string created_at: string updated_at: string } export async function ensureSchema(): Promise { if (schemaReady) return // Legacy document store — left intact after migration (copy-first). await bucket().sql(` CREATE TABLE IF NOT EXISTS skills ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) `) await bucket().sql(` CREATE TABLE IF NOT EXISTS skill_files ( skill_id TEXT NOT NULL, path TEXT NOT NULL, content TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (skill_id, path) ) `) await bucket().sql(` CREATE TABLE IF NOT EXISTS skill_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, skill_id TEXT NOT NULL, path TEXT NOT NULL, content TEXT NOT NULL, replaced_at TEXT NOT NULL ) `) // Migration / backend pointer (legacy rows stay; this records the switch). await bucket().sql(` CREATE TABLE IF NOT EXISTS registry_meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) `) // Fast list/search projection over the plain-repo durable home. await bucket().sql(` CREATE TABLE IF NOT EXISTS skills_index ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, files_json TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) `) // Maps skill_history version_id integers → git commits in the skills repo. await bucket().sql(` CREATE TABLE IF NOT EXISTS skill_revisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, commit_oid TEXT NOT NULL, skill_id TEXT NOT NULL, path TEXT NOT NULL, replaced_at TEXT NOT NULL, content_length INTEGER NOT NULL ) `) schemaReady = true } export function nowIso(): string { return new Date().toISOString() } export async function sqlRows>( query: string, params: Array = [], ): Promise { const result = await bucket().sql(query, params) return (result.rows ?? []) as T[] } export async function sqlRun( query: string, params: Array = [], ): Promise<{ rowsWritten: number }> { const result = await bucket().sql(query, params) return { rowsWritten: result.rowsWritten ?? 0 } } export async function getMeta(key: string): Promise { await ensureSchema() const rows = await sqlRows<{ value: string }>( `SELECT value FROM registry_meta WHERE key = ?`, [key], ) return rows[0]?.value ?? null } export async function setMeta(key: string, value: string): Promise { await ensureSchema() await sqlRun( `INSERT INTO registry_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value], ) } export async function listSkillIds(): Promise { await ensureSchema() const backend = await getMeta('backend') if (backend === 'repo') { const rows = await sqlRows<{ id: string }>( `SELECT id FROM skills_index ORDER BY id ASC`, ) return rows.map((row) => row.id) } const rows = await sqlRows<{ id: string }>( `SELECT id FROM skills ORDER BY id ASC`, ) return rows.map((row) => row.id) } export async function getSkill(id: string): Promise { await ensureSchema() const backend = await getMeta('backend') if (backend === 'repo') { const rows = await sqlRows( `SELECT id, name, description, files_json, created_at, updated_at FROM skills_index WHERE id = ?`, [id], ) const row = rows[0] if (!row) return null return { id: row.id, name: row.name, description: row.description, created_at: row.created_at, updated_at: row.updated_at, } } const rows = await sqlRows( `SELECT id, name, description, created_at, updated_at FROM skills WHERE id = ?`, [id], ) return rows[0] ?? null } export async function getSkillOrThrow(id: string): Promise { const skill = await getSkill(id) if (!skill) { const ids = await listSkillIds() const available = ids.length > 0 ? ids.map((value) => `\`${value}\``).join(', ') : '(none)' throw new Error( `Skill not found: \`${id}\`. Valid skill ids: ${available}. Call skill_list for details.`, ) } return skill } export async function listFilesForSkill( skillId: string, ): Promise { await ensureSchema() return sqlRows( `SELECT skill_id, path, content, updated_at FROM skill_files WHERE skill_id = ? ORDER BY path ASC`, [skillId], ) } export async function getFile( skillId: string, path: string, ): Promise { await ensureSchema() const rows = await sqlRows( `SELECT skill_id, path, content, updated_at FROM skill_files WHERE skill_id = ? AND path = ?`, [skillId, path], ) return rows[0] ?? null } export async function snapshotFile( skillId: string, path: string, content: string, replacedAt: string = nowIso(), ): Promise { await ensureSchema() await sqlRun( `INSERT INTO skill_versions (skill_id, path, content, replaced_at) VALUES (?, ?, ?, ?)`, [skillId, path, content, replacedAt], ) } export async function upsertSkillsIndex(input: { id: string name: string description: string files: string[] created_at: string updated_at: string }): Promise { await ensureSchema() await sqlRun( `INSERT INTO skills_index (id, name, description, files_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, files_json = excluded.files_json, created_at = excluded.created_at, updated_at = excluded.updated_at`, [ input.id, input.name, input.description, JSON.stringify(input.files), input.created_at, input.updated_at, ], ) } export async function deleteSkillsIndex(id: string): Promise { await ensureSchema() await sqlRun(`DELETE FROM skills_index WHERE id = ?`, [id]) } export async function listSkillsIndex(): Promise { await ensureSchema() return sqlRows( `SELECT id, name, description, files_json, created_at, updated_at FROM skills_index ORDER BY id ASC`, ) } export async function insertSkillRevision(input: { id?: number commit_oid: string skill_id: string path: string replaced_at: string content_length: number }): Promise { await ensureSchema() if (input.id != null) { await sqlRun( `INSERT OR REPLACE INTO skill_revisions (id, commit_oid, skill_id, path, replaced_at, content_length) VALUES (?, ?, ?, ?, ?, ?)`, [ input.id, input.commit_oid, input.skill_id, input.path, input.replaced_at, input.content_length, ], ) return input.id } await sqlRun( `INSERT INTO skill_revisions (commit_oid, skill_id, path, replaced_at, content_length) VALUES (?, ?, ?, ?, ?)`, [ input.commit_oid, input.skill_id, input.path, input.replaced_at, input.content_length, ], ) const rows = await sqlRows<{ id: number }>( `SELECT id FROM skill_revisions WHERE commit_oid = ? AND skill_id = ? AND path = ? ORDER BY id DESC LIMIT 1`, [input.commit_oid, input.skill_id, input.path], ) return Number(rows[0]?.id) } export async function getSkillRevision( versionId: number, ): Promise { await ensureSchema() const rows = await sqlRows( `SELECT id, commit_oid, skill_id, path, replaced_at, content_length FROM skill_revisions WHERE id = ?`, [versionId], ) return rows[0] ?? null } export async function listSkillRevisions(input: { skill_id: string path?: string limit: number }): Promise { await ensureSchema() if (input.path) { return sqlRows( `SELECT id, commit_oid, skill_id, path, replaced_at, content_length FROM skill_revisions WHERE skill_id = ? AND path = ? ORDER BY id DESC LIMIT ?`, [input.skill_id, input.path, input.limit], ) } return sqlRows( `SELECT id, commit_oid, skill_id, path, replaced_at, content_length FROM skill_revisions WHERE skill_id = ? ORDER BY id DESC LIMIT ?`, [input.skill_id, input.limit], ) } /** Read-only dump of the legacy SQLite document store (for migration). */ export async function dumpLegacyStore(): Promise<{ skills: SkillRow[] files: SkillFileRow[] versions: SkillVersionRow[] }> { await ensureSchema() const skills = await sqlRows( `SELECT id, name, description, created_at, updated_at FROM skills ORDER BY id ASC`, ) const files = await sqlRows( `SELECT skill_id, path, content, updated_at FROM skill_files ORDER BY skill_id ASC, path ASC`, ) const versions = await sqlRows( `SELECT id, skill_id, path, content, replaced_at FROM skill_versions ORDER BY id ASC`, ) return { skills, files, versions } }