feat: support config skill registration (#9640)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
Spoon
2026-01-29 18:47:06 +01:00
committed by GitHub
parent 5a56e8172f
commit 45ec3105b1
4 changed files with 38 additions and 3 deletions

View File

@@ -560,6 +560,11 @@ export namespace Config {
}) })
export type Command = z.infer<typeof Command> export type Command = z.infer<typeof Command>
export const Skills = z.object({
paths: z.array(z.string()).optional().describe("Additional paths to skill folders"),
})
export type Skills = z.infer<typeof Skills>
export const Agent = z export const Agent = z
.object({ .object({
model: z.string().optional(), model: z.string().optional(),
@@ -895,6 +900,7 @@ export namespace Config {
.record(z.string(), Command) .record(z.string(), Command)
.optional() .optional()
.describe("Command configuration, see https://opencode.ai/docs/commands"), .describe("Command configuration, see https://opencode.ai/docs/commands"),
skills: Skills.optional().describe("Additional skill folder paths"),
watcher: z watcher: z
.object({ .object({
ignore: z.array(z.string()).optional(), ignore: z.array(z.string()).optional(),

View File

@@ -1,5 +1,6 @@
import z from "zod" import z from "zod"
import path from "path" import path from "path"
import os from "os"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
@@ -40,6 +41,7 @@ export namespace Skill {
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
const SKILL_GLOB = new Bun.Glob("**/SKILL.md")
export const state = Instance.state(async () => { export const state = Instance.state(async () => {
const skills: Record<string, Info> = {} const skills: Record<string, Info> = {}
@@ -122,6 +124,25 @@ export namespace Skill {
} }
} }
// Scan additional skill paths from config
const config = await Config.get()
for (const skillPath of config.skills?.paths ?? []) {
const expanded = skillPath.startsWith("~/") ? path.join(os.homedir(), skillPath.slice(2)) : skillPath
const resolved = path.isAbsolute(expanded) ? expanded : path.join(Instance.directory, expanded)
if (!(await Filesystem.isDir(resolved))) {
log.warn("skill path not found", { path: resolved })
continue
}
for await (const match of SKILL_GLOB.scan({
cwd: resolved,
absolute: true,
onlyFiles: true,
followSymlinks: true,
})) {
await addSkill(match)
}
}
return skills return skills
}) })

View File

@@ -62,12 +62,11 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
always: [params.name], always: [params.name],
metadata: {}, metadata: {},
}) })
// Load and parse skill content const content = (await ConfigMarkdown.parse(skill.location)).content
const parsed = await ConfigMarkdown.parse(skill.location)
const dir = path.dirname(skill.location) const dir = path.dirname(skill.location)
// Format output similar to plugin pattern // Format output similar to plugin pattern
const output = [`## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", parsed.content.trim()].join("\n") const output = [`## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", content.trim()].join("\n")
return { return {
title: `Loaded skill: ${skill.name}`, title: `Loaded skill: ${skill.name}`,

View File

@@ -1633,6 +1633,15 @@ export type Config = {
subtask?: boolean subtask?: boolean
} }
} }
/**
* Additional skill folder paths to scan
*/
skills?: {
/**
* Additional paths to skill folders to scan
*/
paths?: Array<string>
}
watcher?: { watcher?: {
ignore?: Array<string> ignore?: Array<string>
} }