feat(skill-loader): add skill loader that converts skills to commands
Skills are loaded from: - ~/.claude/skills/ (user scope) - .claude/skills/ (project scope) Each skill directory contains SKILL.md with frontmatter metadata. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
2
src/features/skill-loader/index.ts
Normal file
2
src/features/skill-loader/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./loader"
|
||||||
85
src/features/skill-loader/loader.ts
Normal file
85
src/features/skill-loader/loader.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { existsSync, readdirSync, readFileSync, statSync, readlinkSync } from "fs"
|
||||||
|
import { homedir } from "os"
|
||||||
|
import { join, resolve } from "path"
|
||||||
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
|
import type { CommandDefinition } from "../command-loader/types"
|
||||||
|
import type { SkillScope, SkillMetadata, LoadedSkillAsCommand } from "./types"
|
||||||
|
|
||||||
|
function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkillAsCommand[] {
|
||||||
|
if (!existsSync(skillsDir)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = readdirSync(skillsDir, { withFileTypes: true })
|
||||||
|
const skills: LoadedSkillAsCommand[] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name.startsWith(".")) continue
|
||||||
|
|
||||||
|
const skillPath = join(skillsDir, entry.name)
|
||||||
|
|
||||||
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
||||||
|
|
||||||
|
let resolvedPath = skillPath
|
||||||
|
if (statSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
||||||
|
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
|
if (!existsSync(skillMdPath)) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(skillMdPath, "utf-8")
|
||||||
|
const { data, body } = parseFrontmatter<SkillMetadata>(content)
|
||||||
|
|
||||||
|
const skillName = data.name || entry.name
|
||||||
|
const originalDescription = data.description || ""
|
||||||
|
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||||
|
|
||||||
|
const wrappedTemplate = `<skill-instruction>
|
||||||
|
${body.trim()}
|
||||||
|
</skill-instruction>
|
||||||
|
|
||||||
|
<user-request>
|
||||||
|
$ARGUMENTS
|
||||||
|
</user-request>`
|
||||||
|
|
||||||
|
const definition: CommandDefinition = {
|
||||||
|
name: skillName,
|
||||||
|
description: formattedDescription,
|
||||||
|
template: wrappedTemplate,
|
||||||
|
model: sanitizeModelField(data.model),
|
||||||
|
}
|
||||||
|
|
||||||
|
skills.push({
|
||||||
|
name: skillName,
|
||||||
|
path: resolvedPath,
|
||||||
|
definition,
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadUserSkillsAsCommands(): Record<string, CommandDefinition> {
|
||||||
|
const userSkillsDir = join(homedir(), ".claude", "skills")
|
||||||
|
const skills = loadSkillsFromDir(userSkillsDir, "user")
|
||||||
|
return skills.reduce((acc, skill) => {
|
||||||
|
acc[skill.name] = skill.definition
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, CommandDefinition>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadProjectSkillsAsCommands(): Record<string, CommandDefinition> {
|
||||||
|
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||||
|
const skills = loadSkillsFromDir(projectSkillsDir, "project")
|
||||||
|
return skills.reduce((acc, skill) => {
|
||||||
|
acc[skill.name] = skill.definition
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, CommandDefinition>)
|
||||||
|
}
|
||||||
16
src/features/skill-loader/types.ts
Normal file
16
src/features/skill-loader/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CommandDefinition } from "../command-loader/types"
|
||||||
|
|
||||||
|
export type SkillScope = "user" | "project"
|
||||||
|
|
||||||
|
export interface SkillMetadata {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
model?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadedSkillAsCommand {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
definition: CommandDefinition
|
||||||
|
scope: SkillScope
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user