| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- import z from "zod"
- import { Config } from "../config/config"
- import { Instance } from "../project/instance"
- import { NamedError } from "@opencode-ai/util/error"
- import { ConfigMarkdown } from "../config/markdown"
- import { Log } from "../util/log"
- import { Global } from "@/global"
- import { Filesystem } from "@/util/filesystem"
- import { exists } from "fs/promises"
- import { Flag } from "@/flag/flag"
- export namespace Skill {
- const log = Log.create({ service: "skill" })
- export const Info = z.object({
- name: z.string(),
- description: z.string(),
- location: z.string(),
- })
- export type Info = z.infer<typeof Info>
- export const InvalidError = NamedError.create(
- "SkillInvalidError",
- z.object({
- path: z.string(),
- message: z.string().optional(),
- issues: z.custom<z.core.$ZodIssue[]>().optional(),
- }),
- )
- export const NameMismatchError = NamedError.create(
- "SkillNameMismatchError",
- z.object({
- path: z.string(),
- expected: z.string(),
- actual: z.string(),
- }),
- )
- const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
- const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
- export const state = Instance.state(async () => {
- const skills: Record<string, Info> = {}
- const addSkill = async (match: string) => {
- const md = await ConfigMarkdown.parse(match)
- if (!md) {
- return
- }
- const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
- if (!parsed.success) return
- // Warn on duplicate skill names
- if (skills[parsed.data.name]) {
- log.warn("duplicate skill name", {
- name: parsed.data.name,
- existing: skills[parsed.data.name].location,
- duplicate: match,
- })
- }
- skills[parsed.data.name] = {
- name: parsed.data.name,
- description: parsed.data.description,
- location: match,
- }
- }
- // Scan .claude/skills/ directories (project-level)
- const claudeDirs = await Array.fromAsync(
- Filesystem.up({
- targets: [".claude"],
- start: Instance.directory,
- stop: Instance.worktree,
- }),
- )
- // Also include global ~/.claude/skills/
- const globalClaude = `${Global.Path.home}/.claude`
- if (await exists(globalClaude)) {
- claudeDirs.push(globalClaude)
- }
- if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS) {
- for (const dir of claudeDirs) {
- const matches = await Array.fromAsync(
- CLAUDE_SKILL_GLOB.scan({
- cwd: dir,
- absolute: true,
- onlyFiles: true,
- followSymlinks: true,
- dot: true,
- }),
- ).catch((error) => {
- log.error("failed .claude directory scan for skills", { dir, error })
- return []
- })
- for (const match of matches) {
- await addSkill(match)
- }
- }
- }
- // Scan .opencode/skill/ directories
- for (const dir of await Config.directories()) {
- for await (const match of OPENCODE_SKILL_GLOB.scan({
- cwd: dir,
- absolute: true,
- onlyFiles: true,
- followSymlinks: true,
- })) {
- await addSkill(match)
- }
- }
- return skills
- })
- export async function get(name: string) {
- return state().then((x) => x[name])
- }
- export async function all() {
- return state().then((x) => Object.values(x))
- }
- }
|