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<SkillMeta>
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<RepoSession> {
try {
await kody.repo_get({ 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.repo_open_session({
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<T>(
fn: (session: RepoSession) => Promise<T>,
conversationId?: string,
): Promise<T> {
const session = await openSkillsRepoSession(conversationId)
try {
return await fn(session)
} finally {
await kody.repo_discard_session({ session_id: session.id }).catch(() => {
// Best effort; unused-session sweep is the platform backstop.
})
}
}
export async function readRepoFile(
sessionId: string,
path: string,
): Promise<string | null> {
const result = await kody.repo_read_file({ session_id: sessionId, path })
return result.content ?? null
}
export async function writeRepoFiles(
sessionId: string,
files: Array<{ path: string; content: string }>,
): Promise<void> {
if (files.length === 0) return
await kody.repo_edit_files({
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<void> {
if (paths.length === 0) return
await kody.repo_edit_files({
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.repo_commit({
session_id: sessionId,
message,
})
const published = await kody.repo_publish_session({ 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.repo_restore({
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<string[]> {
const result = await kody.repo_search({
session_id: sessionId,
pattern: 'name',
glob: '**/skill.json',
output_mode: 'files',
limit: 200,
})
const ids = new Set<string>()
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()
}