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)
}