skill.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import z from "zod"
  2. import { Config } from "../config/config"
  3. import { Instance } from "../project/instance"
  4. import { NamedError } from "@opencode-ai/util/error"
  5. import { ConfigMarkdown } from "../config/markdown"
  6. import { Log } from "../util/log"
  7. import { Global } from "../global"
  8. export namespace Skill {
  9. const log = Log.create({ service: "skill" })
  10. export const Info = z.object({
  11. name: z.string(),
  12. description: z.string(),
  13. location: z.string(),
  14. })
  15. export type Info = z.infer<typeof Info>
  16. export const InvalidError = NamedError.create(
  17. "SkillInvalidError",
  18. z.object({
  19. path: z.string(),
  20. message: z.string().optional(),
  21. issues: z.custom<z.core.$ZodIssue[]>().optional(),
  22. }),
  23. )
  24. export const NameMismatchError = NamedError.create(
  25. "SkillNameMismatchError",
  26. z.object({
  27. path: z.string(),
  28. expected: z.string(),
  29. actual: z.string(),
  30. }),
  31. )
  32. const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
  33. const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
  34. export const state = Instance.state(async () => {
  35. const directories = await Config.directories()
  36. // include the global claude skills
  37. directories.push(Global.Path.home)
  38. const skills: Record<string, Info> = {}
  39. const addSkill = async (match: string) => {
  40. const md = await ConfigMarkdown.parse(match)
  41. if (!md) {
  42. return
  43. }
  44. const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
  45. if (!parsed.success) return
  46. // Warn on duplicate skill names
  47. if (skills[parsed.data.name]) {
  48. log.warn("duplicate skill name", {
  49. name: parsed.data.name,
  50. existing: skills[parsed.data.name].location,
  51. duplicate: match,
  52. })
  53. }
  54. skills[parsed.data.name] = {
  55. name: parsed.data.name,
  56. description: parsed.data.description,
  57. location: match,
  58. }
  59. }
  60. for (const dir of directories) {
  61. for await (const match of OPENCODE_SKILL_GLOB.scan({
  62. cwd: dir,
  63. absolute: true,
  64. onlyFiles: true,
  65. followSymlinks: true,
  66. })) {
  67. await addSkill(match)
  68. }
  69. for await (const match of CLAUDE_SKILL_GLOB.scan({
  70. cwd: dir,
  71. absolute: true,
  72. onlyFiles: true,
  73. followSymlinks: true,
  74. dot: true,
  75. })) {
  76. await addSkill(match)
  77. }
  78. }
  79. return skills
  80. })
  81. export async function get(name: string) {
  82. return state().then((x) => x[name])
  83. }
  84. export async function all() {
  85. return state().then((x) => Object.values(x))
  86. }
  87. }