Skip to content
← Public packages

@kentcdodds/skills

Store, retrieve, edit, version, and revert reusable agent skill documents via skillList then skillGet.

src/skill-list.ts

110 lines · 2.6 KB · TypeScript
import { getBackend } from './backend.ts'
import {
	ensureSchema,
	listFilesForSkill,
	listSkillsIndex,
	sqlRows,
} from './db.ts'
import {
	listSkillIdsFromRepo,
	parseSkillMeta,
	readRepoFile,
	skillMetaPath,
	withSkillsRepoSession,
} from './repo.ts'

export type SkillListItem = {
	id: string
	name: string
	description: string
	files: string[]
	updatedAt: string
}

type SqlLegacySkill = {
	id: string
	name: string
	description: string
	created_at: string
	updated_at: string
}

async function listFromLegacy(): Promise<SkillListItem[]> {
	await ensureSchema()
	const skills = await sqlRows<SqlLegacySkill>(
		`SELECT id, name, description, created_at, updated_at
		 FROM skills
		 ORDER BY id ASC`,
	)

	const items: SkillListItem[] = []
	for (const skill of skills) {
		const files = await listFilesForSkill(skill.id)
		items.push({
			id: skill.id,
			name: skill.name,
			description: skill.description,
			files: files.map((file) => file.path),
			updatedAt: skill.updated_at,
		})
	}
	return items
}

async function listFromRepo(): Promise<SkillListItem[]> {
	const indexed = await listSkillsIndex()
	if (indexed.length > 0) {
		return indexed.map((row) => {
			let files: string[] = []
			try {
				const parsed = JSON.parse(row.filesJson) as unknown
				if (Array.isArray(parsed)) {
					files = parsed.filter((value): value is string => typeof value === 'string')
				}
			} catch {
				files = []
			}
			return {
				id: row.id,
				name: row.name,
				description: row.description,
				files,
				updatedAt: row.updatedAt,
			}
		})
	}

	// Index empty (fresh install / before migrate sync): discover from repo.
	return withSkillsRepoSession(async (session) => {
		const ids = await listSkillIdsFromRepo(session.id)
		const items: SkillListItem[] = []
		for (const id of ids) {
			const meta = parseSkillMeta(
				await readRepoFile(session.id, skillMetaPath(id)),
			)
			if (!meta) continue
			items.push({
				id,
				name: meta.name,
				description: meta.description,
				files: meta.files,
				updatedAt: meta.updatedAt,
			})
		}
		return items
	})
}

/**
 * List all skills in the registry with descriptions and file paths.
 * This is the agents' index — descriptions are complete enough to choose a skill.
 *
 * @example
 * import skillList from 'kody:@kentcdodds/skills/skill-list'
 * const skills = await skillList()
 * // => [{ id: 'kent-writing-voice', name: '...', description: '...', files: ['SKILL.md', ...], updatedAt }]
 */
export default async function skillList(): Promise<SkillListItem[]> {
	const backend = await getBackend()
	return backend === 'repo' ? listFromRepo() : listFromLegacy()
}