import { kody } from 'kody:runtime' /** Plain Kody repo that is the durable home for skill documents. */ export const SKILLS_REPO_NAME = 'skills' export const META_FILE = 'skill.json' export type SkillMeta = { name: string description: string created_at: string updated_at: string files: string[] } export type RepoSession = { id: string base_commit: string } export function skillDir(skillId: string): string { return skillId } export function skillMetaPath(skillId: string): string { return `${skillId}/${META_FILE}` } export function skillFilePath(skillId: string, path: string): string { return `${skillId}/${path}` } export function parseSkillMeta(content: string | null | undefined): SkillMeta | null { if (!content) return null try { const parsed = JSON.parse(content) as Partial if ( typeof parsed.name !== 'string' || typeof parsed.description !== 'string' || typeof parsed.created_at !== 'string' || typeof parsed.updated_at !== 'string' ) { return null } const files = Array.isArray(parsed.files) ? parsed.files.filter((value): value is string => typeof value === 'string') : [] return { name: parsed.name, description: parsed.description, created_at: parsed.created_at, updated_at: parsed.updated_at, files, } } catch { return null } } export function serializeSkillMeta(meta: SkillMeta): string { return `${JSON.stringify(meta, null, '\t')}\n` } /** * Open (or resume) an MCP file-level session against the skills plain repo. * Plain repos are live-at-HEAD; pair writes with repo_commit + repo_publish_session. */ export async function openSkillsRepoSession( conversationId?: string, ): Promise { try { await kody.repoGet({ name: SKILLS_REPO_NAME }) } catch { throw new Error( `Plain repo \`${SKILLS_REPO_NAME}\` not found. Create it with repo_create({ name: "${SKILLS_REPO_NAME}" }), push an initial commit via repo_get_git_remote, then retry.`, ) } try { const session = await kody.repoOpenSession({ target: { kind: 'repo', name: SKILLS_REPO_NAME }, ...(conversationId ? { conversation_id: conversationId } : {}), }) return { id: session.id, base_commit: session.base_commit, } } catch (error) { const message = error instanceof Error ? error.message : String(error) if (message.includes('no commits yet')) { throw new Error( `Plain repo \`${SKILLS_REPO_NAME}\` has no commits yet. Push an initial commit through repo_get_git_remote before using skill_* exports.`, ) } throw error } } /** * Open a skills-repo session, run `fn`, then discard. One-shot skill_* * helpers must not leave active sessions against the repo_sessions entitlement. */ export async function withSkillsRepoSession( fn: (session: RepoSession) => Promise, conversationId?: string, ): Promise { const session = await openSkillsRepoSession(conversationId) try { return await fn(session) } finally { await kody.repoDiscardSession({ session_id: session.id }).catch(() => { // Best effort; unused-session sweep is the platform backstop. }) } } export async function readRepoFile( sessionId: string, path: string, ): Promise { const result = await kody.repoReadFile({ session_id: sessionId, path }) return result.content ?? null } export async function writeRepoFiles( sessionId: string, files: Array<{ path: string; content: string }>, ): Promise { if (files.length === 0) return await kody.repoEditFiles({ session_id: sessionId, edits: files.map((file) => ({ kind: 'write' as const, path: file.path, content: file.content, })), }) } export async function deleteRepoPaths( sessionId: string, paths: string[], ): Promise { if (paths.length === 0) return await kody.repoEditFiles({ session_id: sessionId, edits: paths.map((path) => ({ kind: 'delete' as const, path })), }) } export async function commitAndPublish( sessionId: string, message: string, ): Promise<{ oid: string }> { const commit = await kody.repoCommit({ session_id: sessionId, message, }) const published = await kody.repoPublishSession({ session_id: sessionId }) if (published.status !== 'ok') { throw new Error( `repo_publish_session failed (${published.status}): ${published.message}`, ) } return { oid: commit.oid } } export async function restoreAndPublish(input: { sessionId: string paths: string[] commit: string message: string }): Promise<{ oid: string }> { await kody.repoRestore({ session_id: input.sessionId, paths: input.paths, commit: input.commit, }) return commitAndPublish(input.sessionId, input.message) } /** Discover skill ids by finding skill.json files under each skill directory. */ export async function listSkillIdsFromRepo(sessionId: string): Promise { const result = await kody.repoSearch({ session_id: sessionId, pattern: 'name', glob: '**/skill.json', output_mode: 'files', limit: 200, }) const ids = new Set() for (const file of result.files ?? []) { const path = String(file.path ?? '') const parts = path.split('/') if (parts.length === 2 && parts[1] === META_FILE && parts[0]) { ids.add(parts[0]) } } return [...ids].sort() }