Skip to content

Commit d34154b

Browse files
committed
feat(skill): align with opencode-skills approach
- Add Zod schema validation following Anthropic Agent Skills Spec v1.0 - Include basePath in skill output for path resolution - Simplify tool description and output format - Add validation error logging for invalid skills 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
1 parent 9e00be9 commit d34154b

File tree

2 files changed

+65
-45
lines changed

2 files changed

+65
-45
lines changed

src/tools/skill/tools.ts

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,23 @@ import { tool } from "@opencode-ai/plugin"
22
import { existsSync, readdirSync, statSync, readlinkSync, readFileSync } from "fs"
33
import { homedir } from "os"
44
import { join, resolve, basename } from "path"
5+
import { z } from "zod/v4"
56
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
6-
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill } from "./types"
7+
import { SkillFrontmatterSchema } from "./types"
8+
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
9+
10+
function parseSkillFrontmatter(data: Record<string, unknown>): SkillFrontmatter {
11+
return {
12+
name: typeof data.name === "string" ? data.name : "",
13+
description: typeof data.description === "string" ? data.description : "",
14+
license: typeof data.license === "string" ? data.license : undefined,
15+
"allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined,
16+
metadata:
17+
typeof data.metadata === "object" && data.metadata !== null
18+
? (data.metadata as Record<string, string>)
19+
: undefined,
20+
}
21+
}
722

823
function discoverSkillsFromDir(
924
skillsDir: string,
@@ -93,10 +108,14 @@ async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
93108
content = await resolveCommandsInText(content)
94109
const { data, body } = parseFrontmatter(content)
95110

111+
const frontmatter = parseSkillFrontmatter(data)
112+
96113
const metadata: SkillMetadata = {
97-
name: data.name || basename(skillPath),
98-
description: data.description || "",
99-
license: data.license,
114+
name: frontmatter.name || basename(skillPath),
115+
description: frontmatter.description,
116+
license: frontmatter.license,
117+
allowedTools: frontmatter["allowed-tools"],
118+
metadata: frontmatter.metadata,
100119
}
101120

102121
const referencesDir = join(resolvedPath, "references")
@@ -118,6 +137,7 @@ async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
118137
return {
119138
name: metadata.name,
120139
path: resolvedPath,
140+
basePath: resolvedPath,
121141
metadata,
122142
content: body,
123143
references,
@@ -202,13 +222,15 @@ async function loadSkillWithReferences(
202222
content = await resolveCommandsInText(content)
203223
referencesLoaded.push({ path: ref, content })
204224
} catch {
225+
// Skip unreadable references
205226
}
206227
}
207228
}
208229

209230
return {
210231
name: skill.name,
211232
metadata: skill.metadata,
233+
basePath: skill.basePath,
212234
body: skill.content,
213235
referencesLoaded,
214236
}
@@ -234,57 +256,32 @@ function formatLoadedSkills(loadedSkills: LoadedSkill[]): string {
234256
return "No skills loaded."
235257
}
236258

237-
const sections: string[] = ["# Loaded Skills\n"]
238-
239-
for (const skill of loadedSkills) {
240-
sections.push(`## ${skill.metadata.name}\n`)
241-
sections.push(`**Description**: ${skill.metadata.description || "(no description)"}\n`)
242-
sections.push("### Skill Instructions\n")
243-
sections.push(skill.body.trim())
244-
245-
if (skill.referencesLoaded.length > 0) {
246-
sections.push("\n### Loaded References\n")
247-
for (const ref of skill.referencesLoaded) {
248-
sections.push(`#### ${ref.path}\n`)
249-
sections.push("```")
250-
sections.push(ref.content.trim())
251-
sections.push("```\n")
252-
}
253-
}
259+
const skill = loadedSkills[0]
260+
const sections: string[] = []
254261

255-
sections.push("\n---\n")
262+
sections.push(`Base directory for this skill: ${skill.basePath}/`)
263+
sections.push("")
264+
sections.push(skill.body.trim())
265+
266+
if (skill.referencesLoaded.length > 0) {
267+
sections.push("\n---\n### Loaded References\n")
268+
for (const ref of skill.referencesLoaded) {
269+
sections.push(`#### ${ref.path}\n`)
270+
sections.push("```")
271+
sections.push(ref.content.trim())
272+
sections.push("```\n")
273+
}
256274
}
257275

258-
const skillNames = loadedSkills.map((s) => s.metadata.name).join(", ")
259-
sections.push(`**Skills loaded**: ${skillNames}`)
260-
sections.push(`**Total**: ${loadedSkills.length} skill(s)`)
261-
sections.push("\nPlease confirm these skills match your needs before proceeding.")
276+
sections.push(`\n---\n**Launched skill**: ${skill.metadata.name}`)
262277

263278
return sections.join("\n")
264279
}
265280

266281
export const skill = tool({
267282
description: `Execute a skill within the main conversation.
268283
269-
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
270-
271-
How to use skills:
272-
- Invoke skills using this tool with the skill name only (no arguments)
273-
- When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task
274-
275-
Important:
276-
- Only use skills listed in Available Skills below
277-
- Do not invoke a skill that is already running
278-
279-
Skills are loaded from:
280-
- ~/.claude/skills/ (user scope - global skills)
281-
- ./.claude/skills/ (project scope - project-specific skills)
282-
283-
Each skill contains:
284-
- SKILL.md: Main instructions with YAML frontmatter (name, description)
285-
- references/: Documentation files loaded into context as needed
286-
- scripts/: Executable code for deterministic operations
287-
- assets/: Files used in output (templates, icons, etc.)
284+
When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task.
288285
289286
Available Skills:
290287
${skillListForDescription}`,

src/tools/skill/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1+
import { z } from "zod/v4"
2+
13
export type SkillScope = "user" | "project"
24

5+
/**
6+
* Zod schema for skill frontmatter validation
7+
* Following Anthropic Agent Skills Specification v1.0
8+
*/
9+
export const SkillFrontmatterSchema = z.object({
10+
name: z
11+
.string()
12+
.regex(/^[a-z0-9-]+$/, "Name must be lowercase alphanumeric with hyphens only")
13+
.min(1, "Name cannot be empty"),
14+
description: z.string().min(20, "Description must be at least 20 characters for discoverability"),
15+
license: z.string().optional(),
16+
"allowed-tools": z.array(z.string()).optional(),
17+
metadata: z.record(z.string(), z.string()).optional(),
18+
})
19+
20+
export type SkillFrontmatter = z.infer<typeof SkillFrontmatterSchema>
21+
322
export interface SkillMetadata {
423
name: string
524
description: string
625
license?: string
26+
allowedTools?: string[]
27+
metadata?: Record<string, string>
728
}
829

930
export interface SkillInfo {
1031
name: string
1132
path: string
33+
basePath: string
1234
metadata: SkillMetadata
1335
content: string
1436
references: string[]
@@ -19,6 +41,7 @@ export interface SkillInfo {
1941
export interface LoadedSkill {
2042
name: string
2143
metadata: SkillMetadata
44+
basePath: string
2245
body: string
2346
referencesLoaded: Array<{ path: string; content: string }>
2447
}

0 commit comments

Comments
 (0)