Skip to content

Instantly share code, notes, and snippets.

@mezotv
Last active January 20, 2026 13:04
Show Gist options
  • Select an option

  • Save mezotv/3b746b99b1ae24ed5f1052ce93b18041 to your computer and use it in GitHub Desktop.

Select an option

Save mezotv/3b746b99b1ae24ed5f1052ce93b18041 to your computer and use it in GitHub Desktop.
AI SDK V6 Skill Tool
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");
};
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