| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- import path from "path"
- import { pathToFileURL } from "url"
- import z from "zod"
- import { Tool } from "./tool"
- import { Skill } from "../skill"
- import { PermissionNext } from "../permission/next"
- import { Ripgrep } from "../file/ripgrep"
- import { iife } from "@/util/iife"
- export const SkillTool = Tool.define("skill", async (ctx) => {
- const skills = await Skill.all()
- // Filter skills by agent permissions if agent provided
- const agent = ctx?.agent
- const accessibleSkills = agent
- ? skills.filter((skill) => {
- const rule = PermissionNext.evaluate("skill", skill.name, agent.permission)
- return rule.action !== "deny"
- })
- : skills
- const description =
- accessibleSkills.length === 0
- ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
- : [
- "Load a specialized skill that provides domain-specific instructions and workflows.",
- "",
- "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
- "",
- "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
- "",
- 'Tool output includes a `<skill_content name="...">` block with the loaded content.',
- "",
- "The following skills provide specialized sets of instructions for particular tasks",
- "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
- "",
- "<available_skills>",
- ...accessibleSkills.flatMap((skill) => [
- ` <skill>`,
- ` <name>${skill.name}</name>`,
- ` <description>${skill.description}</description>`,
- ` <location>${pathToFileURL(skill.location).href}</location>`,
- ` </skill>`,
- ]),
- "</available_skills>",
- ].join("\n")
- const examples = accessibleSkills
- .map((skill) => `'${skill.name}'`)
- .slice(0, 3)
- .join(", ")
- const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : ""
- const parameters = z.object({
- name: z.string().describe(`The name of the skill from available_skills${hint}`),
- })
- return {
- description,
- parameters,
- async execute(params: z.infer<typeof parameters>, ctx) {
- const skill = await Skill.get(params.name)
- if (!skill) {
- const available = await Skill.all().then((x) => Object.keys(x).join(", "))
- throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
- }
- await ctx.ask({
- permission: "skill",
- patterns: [params.name],
- always: [params.name],
- metadata: {},
- })
- const dir = path.dirname(skill.location)
- const base = pathToFileURL(dir).href
- const limit = 10
- const files = await iife(async () => {
- const arr = []
- for await (const file of Ripgrep.files({
- cwd: dir,
- follow: false,
- hidden: true,
- signal: ctx.abort,
- })) {
- if (file.includes("SKILL.md")) {
- continue
- }
- arr.push(path.resolve(dir, file))
- if (arr.length >= limit) {
- break
- }
- }
- return arr
- }).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))
- return {
- title: `Loaded skill: ${skill.name}`,
- output: [
- `<skill_content name="${skill.name}">`,
- `# Skill: ${skill.name}`,
- "",
- skill.content.trim(),
- "",
- `Base directory for this skill: ${base}`,
- "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
- "Note: file list is sampled.",
- "",
- "<skill_files>",
- files,
- "</skill_files>",
- "</skill_content>",
- ].join("\n"),
- metadata: {
- name: skill.name,
- dir,
- },
- }
- },
- }
- })
|