feat(skill): re-read skills and commands from disk on every invocation
Removes in-memory caching so newly created skills mid-session are immediately available via skill(). Clears the module-level skill cache before each getAllSkills() call. Pre-provided skills from options are merged as fallbacks for test compatibility.
This commit is contained in:
@@ -486,3 +486,42 @@ describe("skill tool - ordering and priority", () => {
|
||||
expect(tool.description).toContain("/test-cmd")
|
||||
})
|
||||
})
|
||||
|
||||
describe("skill tool - dynamic discovery", () => {
|
||||
it("discovers skills from disk on every invocation instead of caching", async () => {
|
||||
// given: tool created with initial skills
|
||||
const initialSkills = [createMockSkill("initial-skill")]
|
||||
const tool = createSkillTool({ skills: initialSkills })
|
||||
|
||||
// when: executing with the initial skill name
|
||||
const result = await tool.execute({ name: "initial-skill" }, mockContext)
|
||||
|
||||
// then: initial skill found (merged from options.skills since not on disk)
|
||||
expect(result).toContain("Skill: initial-skill")
|
||||
})
|
||||
|
||||
it("merges pre-provided skills with dynamically discovered ones", async () => {
|
||||
// given: tool with a synthetic skill not on disk
|
||||
const syntheticSkill = createMockSkill("synthetic-only")
|
||||
const tool = createSkillTool({ skills: [syntheticSkill] })
|
||||
|
||||
// when: looking up the synthetic skill
|
||||
const result = await tool.execute({ name: "synthetic-only" }, mockContext)
|
||||
|
||||
// then: synthetic skill is still accessible via merge
|
||||
expect(result).toContain("Skill: synthetic-only")
|
||||
})
|
||||
|
||||
it("prefers disk-discovered skills over pre-provided ones", async () => {
|
||||
// given: tool with a pre-provided skill that also exists on disk (builtin)
|
||||
const overrideSkill = createMockSkill("playwright")
|
||||
overrideSkill.definition.description = "SHOULD_BE_OVERRIDDEN"
|
||||
const tool = createSkillTool({ skills: [overrideSkill] })
|
||||
|
||||
// when: executing with the builtin skill name
|
||||
const result = await tool.execute({ name: "playwright" }, mockContext)
|
||||
|
||||
// then: disk version wins (not the pre-provided override)
|
||||
expect(result).not.toContain("SHOULD_BE_OVERRIDDEN")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import { TOOL_DESCRIPTION_NO_SKILLS, TOOL_DESCRIPTION_PREFIX } from "./constants"
|
||||
import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import { getAllSkills, extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content"
|
||||
import { getAllSkills, extractSkillTemplate, clearSkillCache } from "../../features/opencode-skill-loader/skill-content"
|
||||
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
|
||||
import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager"
|
||||
import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js"
|
||||
@@ -184,24 +184,22 @@ async function formatMcpCapabilities(
|
||||
}
|
||||
|
||||
export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition {
|
||||
let cachedSkills: LoadedSkill[] | null = null
|
||||
let cachedCommands: CommandInfo[] | null = options.commands ?? null
|
||||
let cachedDescription: string | null = null
|
||||
|
||||
const getSkills = async (): Promise<LoadedSkill[]> => {
|
||||
if (options.skills) return options.skills
|
||||
if (cachedSkills) return cachedSkills
|
||||
cachedSkills = await getAllSkills({disabledSkills: options?.disabledSkills})
|
||||
return cachedSkills
|
||||
clearSkillCache()
|
||||
const discovered = await getAllSkills({disabledSkills: options?.disabledSkills})
|
||||
if (!options.skills) return discovered
|
||||
const discoveredNames = new Set(discovered.map(s => s.name))
|
||||
const extras = options.skills.filter(s => !discoveredNames.has(s.name))
|
||||
return [...discovered, ...extras]
|
||||
}
|
||||
|
||||
const getCommands = (): CommandInfo[] => {
|
||||
if (cachedCommands) return cachedCommands
|
||||
cachedCommands = discoverCommandsSync(undefined, {
|
||||
return discoverCommandsSync(undefined, {
|
||||
pluginsEnabled: options.pluginsEnabled,
|
||||
enabledPluginsOverride: options.enabledPluginsOverride,
|
||||
})
|
||||
return cachedCommands
|
||||
}
|
||||
|
||||
const buildDescription = async (): Promise<string> => {
|
||||
|
||||
Reference in New Issue
Block a user