Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/skills

src/skill-get.ts

166 lines · 3.8 KB · TypeScript
import { getBackend } from './backend.ts'
import {
	getFile,
	getSkillOrThrow,
	listFilesForSkill,
	listSkillIds,
} from './db.ts'
import {
	parseSkillMeta,
	readRepoFile,
	skillFilePath,
	skillMetaPath,
	withSkillsRepoSession,
} from './repo.ts'

export type SkillGetInput = {
	id: string
	path?: string
}

export type SkillFileContent = {
	path: string
	content: string
}

export type SkillGetResult =
	| {
			id: string
			name: string
			description: string
			created_at: string
			updated_at: string
			files: SkillFileContent[]
	  }
	| {
			id: string
			name: string
			description: string
			path: string
			content: string
			updated_at: string
	  }

async function getFromLegacy(
	id: string,
	path: string,
): Promise<SkillGetResult> {
	const skill = await getSkillOrThrow(id)

	if (!path) {
		const files = await listFilesForSkill(id)
		return {
			id: skill.id,
			name: skill.name,
			description: skill.description,
			created_at: skill.created_at,
			updated_at: skill.updated_at,
			files: files.map((file) => ({
				path: file.path,
				content: file.content,
			})),
		}
	}

	const file = await getFile(id, path)
	if (!file) {
		const files = await listFilesForSkill(id)
		const available =
			files.length > 0
				? files.map((row) => `\`${row.path}\``).join(', ')
				: '(none)'
		throw new Error(
			`File not found: \`${path}\` in skill \`${id}\`. Valid paths: ${available}.`,
		)
	}

	return {
		id: skill.id,
		name: skill.name,
		description: skill.description,
		path: file.path,
		content: file.content,
		updated_at: file.updated_at,
	}
}

async function getFromRepo(id: string, path: string): Promise<SkillGetResult> {
	return withSkillsRepoSession(async (session) => {
		const meta = parseSkillMeta(await readRepoFile(session.id, skillMetaPath(id)))
		if (!meta) {
			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.`,
			)
		}

		if (!path) {
			const files: SkillFileContent[] = []
			for (const filePath of meta.files) {
				const content = await readRepoFile(
					session.id,
					skillFilePath(id, filePath),
				)
				if (content == null) continue
				files.push({ path: filePath, content })
			}
			return {
				id,
				name: meta.name,
				description: meta.description,
				created_at: meta.created_at,
				updated_at: meta.updated_at,
				files,
			}
		}

		const content = await readRepoFile(session.id, skillFilePath(id, path))
		if (content == null) {
			const available =
				meta.files.length > 0
					? meta.files.map((value) => `\`${value}\``).join(', ')
					: '(none)'
			throw new Error(
				`File not found: \`${path}\` in skill \`${id}\`. Valid paths: ${available}.`,
			)
		}

		return {
			id,
			name: meta.name,
			description: meta.description,
			path,
			content,
			updated_at: meta.updated_at,
		}
	})
}

/**
 * Read a skill. Without `path`, returns metadata plus all files.
 * With `path`, returns that one file.
 *
 * @example
 * import skillGet from 'kody:@kody/skills/skill-get'
 * const skill = await skillGet({ id: 'writing-style' })
 * const file = await skillGet({ id: 'writing-style', path: 'SKILL.md' })
 */
export default async function skillGet(
	params: SkillGetInput = {} as SkillGetInput,
): Promise<SkillGetResult> {
	const id = String(params.id ?? '').trim()
	if (!id) {
		const ids = await listSkillIds()
		const available =
			ids.length > 0 ? ids.map((value) => `\`${value}\``).join(', ') : '(none)'
		throw new Error(
			`skill_get requires \`id\`. Valid skill ids: ${available}. Call skill_list for details.`,
		)
	}

	const path = params.path == null ? '' : String(params.path).trim()
	const backend = await getBackend()
	return backend === 'repo' ? getFromRepo(id, path) : getFromLegacy(id, path)
}