Last active
January 20, 2026 13:04
-
-
Save mezotv/3b746b99b1ae24ed5f1052ce93b18041 to your computer and use it in GitHub Desktop.
AI SDK V6 Skill Tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import dedent from "dedent"; | |
| interface ToolDescription { | |
| intro: string; | |
| toolName: string; | |
| whenToUse?: string; | |
| whenNotToUse?: string; | |
| usageNotes?: string; | |
| } | |
| export const toolDescription = (input: ToolDescription) => { | |
| const intro = input.intro ? dedent(input.intro) : undefined; | |
| const whenToUse = input.whenToUse ? dedent(input.whenToUse) : undefined; | |
| const whenNotToUse = input.whenNotToUse | |
| ? dedent(input.whenNotToUse) | |
| : undefined; | |
| const usageNotes = input.usageNotes ? dedent(input.usageNotes) : undefined; | |
| const toolName = input.toolName; | |
| const parts: string[] = []; | |
| if (intro) { | |
| parts.push(intro); | |
| } | |
| if (whenToUse) { | |
| parts.push(`**When to use the ${toolName} tool**\n${whenToUse}`); | |
| } | |
| if (whenNotToUse) { | |
| parts.push(`**When NOT to use the ${toolName} tool**\n${whenNotToUse}`); | |
| } | |
| if (usageNotes) { | |
| parts.push(`**Usage notes**\n${usageNotes}`); | |
| } | |
| return parts.join("\n\n"); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { type Tool, tool } from "ai"; | |
| import matter from "gray-matter"; | |
| import * as z from "zod"; | |
| import { toolDescription } from "../utils/description"; | |
| interface SkillMetadata { | |
| name: string; | |
| version?: string; | |
| description?: string; | |
| "allowed-tools"?: string[]; | |
| folder: string; | |
| filename: string; | |
| } | |
| interface Skill extends SkillMetadata { | |
| content: string; | |
| } | |
| function parseSkillFrontmatter(content: string): Partial<SkillMetadata> { | |
| const parsed = matter(content); | |
| return { | |
| name: parsed.data.name, | |
| version: parsed.data.version, | |
| description: parsed.data.description, | |
| "allowed-tools": parsed.data["allowed-tools"], | |
| }; | |
| } | |
| function getSkillMetadata( | |
| skillFolder: string, | |
| skillsDir: string, | |
| ): SkillMetadata { | |
| const skillPath = path.join(skillsDir, skillFolder); | |
| const skillMdPath = path.join(skillPath, "SKILL.md"); | |
| if (!fs.existsSync(skillMdPath)) { | |
| return { name: skillFolder, folder: skillFolder, filename: "SKILL.md" }; | |
| } | |
| const content = fs.readFileSync(skillMdPath, "utf-8"); | |
| const metadata = parseSkillFrontmatter(content); | |
| return { | |
| name: metadata.name || skillFolder, | |
| version: metadata.version, | |
| description: metadata.description, | |
| "allowed-tools": metadata["allowed-tools"], | |
| folder: skillFolder, | |
| filename: "SKILL.md", | |
| }; | |
| } | |
| export function listAvailableSkills(): Tool { | |
| return tool({ | |
| description: toolDescription({ | |
| toolName: "list_available_skills", | |
| intro: "Lists all available skills for the user.", | |
| whenToUse: | |
| "When user asks about available skills, wants to see a list of skills, or needs to check if a skill is available.", | |
| usageNotes: `Requires the user to be logged in and have access to the skills. | |
| Returns a list of available skills with their metadata (name, version, description, allowed-tools, folder).`, | |
| }), | |
| inputSchema: z.object({ | |
| limit: z.number().default(10).describe("The number of skills to list"), | |
| offset: z | |
| .number() | |
| .default(0) | |
| .describe("The offset to start listing skills from"), | |
| }), | |
| execute: async ({ limit, offset }) => { | |
| const skillsDir = getSkillsDir(); | |
| const skillFolders = fs | |
| .readdirSync(skillsDir, { withFileTypes: true }) | |
| .filter((dirent) => dirent.isDirectory()) | |
| .map((dirent) => dirent.name); | |
| const skills = skillFolders | |
| .slice(offset, offset + limit) | |
| .map((folder) => getSkillMetadata(folder, skillsDir)); | |
| return { | |
| skills, | |
| total: skillFolders.length, | |
| }; | |
| }, | |
| }); | |
| } | |
| function getSkillsDir(): string { | |
| return path.join(process.cwd(), "apps", "dashboard", "src", "lib", "ai", "skills"); | |
| } | |
| export function getSkillByName(): Tool { | |
| return tool({ | |
| description: toolDescription({ | |
| toolName: "get_skill_by_name", | |
| intro: | |
| "Gets a specific skill by its name or folder name. Use list_available_skills to see all available skills first.", | |
| whenToUse: | |
| "When user asks about a specific skill, wants to see skill details, or needs to use a particular skill. Use list_available_skills first to find the correct skill name.", | |
| usageNotes: `Requires the user to be logged in and have access to the skills. | |
| Use list_available_skills to see all available skills and their names before calling this tool. | |
| Returns the full skill metadata and content including name, version, description, allowed-tools, folder, filename, and the complete skill content.`, | |
| }), | |
| inputSchema: z.object({ | |
| name: z | |
| .string() | |
| .describe( | |
| "The name of the skill to get. Can be the skill's name from frontmatter or the folder name.", | |
| ), | |
| }), | |
| execute: async ({ name }) => { | |
| const skillsDir = getSkillsDir(); | |
| const skillFolders = fs | |
| .readdirSync(skillsDir, { withFileTypes: true }) | |
| .filter((dirent) => dirent.isDirectory()) | |
| .map((dirent) => dirent.name); | |
| let skillFolder: string | undefined; | |
| for (const folder of skillFolders) { | |
| const skillPath = path.join(skillsDir, folder); | |
| const skillMdPath = path.join(skillPath, "SKILL.md"); | |
| if (fs.existsSync(skillMdPath)) { | |
| const content = fs.readFileSync(skillMdPath, "utf-8"); | |
| const parsed = matter(content); | |
| const skillName = parsed.data.name; | |
| if ( | |
| folder.toLowerCase() === name.toLowerCase() || | |
| skillName?.toLowerCase() === name.toLowerCase() | |
| ) { | |
| skillFolder = folder; | |
| break; | |
| } | |
| } else if (folder.toLowerCase() === name.toLowerCase()) { | |
| skillFolder = folder; | |
| break; | |
| } | |
| } | |
| if (!skillFolder) { | |
| return { | |
| error: `Skill "${name}" not found. Use list_available_skills to see all available skills.`, | |
| }; | |
| } | |
| const skillPath = path.join(skillsDir, skillFolder); | |
| const skillMdPath = path.join(skillPath, "SKILL.md"); | |
| if (!fs.existsSync(skillMdPath)) { | |
| return { | |
| error: `Skill file not found for "${skillFolder}".`, | |
| }; | |
| } | |
| const content = fs.readFileSync(skillMdPath, "utf-8"); | |
| const parsed = matter(content); | |
| const metadata = parseSkillFrontmatter(content); | |
| return { | |
| name: metadata.name || skillFolder, | |
| version: metadata.version, | |
| description: metadata.description, | |
| "allowed-tools": metadata["allowed-tools"], | |
| folder: skillFolder, | |
| filename: "SKILL.md", | |
| content: parsed.content, | |
| } satisfies Skill; | |
| }, | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment