import { createBuiltinSkills } from "../builtin-skills/skills" import { discoverSkills } from "./loader" import type { LoadedSkill } from "./types" import { parseFrontmatter } from "../../shared/frontmatter" import { readFileSync } from "node:fs" import type { GitMasterConfig, BrowserAutomationProvider } from "../../config/schema" export interface SkillResolutionOptions { gitMasterConfig?: GitMasterConfig browserProvider?: BrowserAutomationProvider disabledSkills?: Set } const cachedSkillsByProvider = new Map() function clearSkillCache(): void { cachedSkillsByProvider.clear() } async function getAllSkills(options?: SkillResolutionOptions): Promise { const cacheKey = options?.browserProvider ?? "playwright" const hasDisabledSkills = options?.disabledSkills && options.disabledSkills.size > 0 // Skip cache if disabledSkills is provided (varies between calls) if (!hasDisabledSkills) { const cached = cachedSkillsByProvider.get(cacheKey) if (cached) return cached } const [discoveredSkills, builtinSkillDefs] = await Promise.all([ discoverSkills({ includeClaudeCodePaths: true }), Promise.resolve( createBuiltinSkills({ browserProvider: options?.browserProvider, disabledSkills: options?.disabledSkills, }) ), ]) const builtinSkillsAsLoaded: LoadedSkill[] = builtinSkillDefs.map((skill) => ({ name: skill.name, definition: { name: skill.name, description: skill.description, template: skill.template, model: skill.model, agent: skill.agent, subtask: skill.subtask, }, scope: "builtin" as const, license: skill.license, compatibility: skill.compatibility, metadata: skill.metadata as Record | undefined, allowedTools: skill.allowedTools, mcpConfig: skill.mcpConfig, })) // Provider-gated skill names that should be filtered based on browserProvider const providerGatedSkillNames = new Set(["agent-browser", "playwright"]) const browserProvider = options?.browserProvider ?? "playwright" // Filter discovered skills to exclude provider-gated names that don't match the selected provider const filteredDiscoveredSkills = discoveredSkills.filter((skill) => { if (!providerGatedSkillNames.has(skill.name)) { return true } // For provider-gated skills, only include if it matches the selected provider return skill.name === browserProvider }) const discoveredNames = new Set(filteredDiscoveredSkills.map((s) => s.name)) const uniqueBuiltins = builtinSkillsAsLoaded.filter((s) => !discoveredNames.has(s.name)) let allSkills = [...filteredDiscoveredSkills, ...uniqueBuiltins] // Filter discovered skills by disabledSkills (builtin skills are already filtered by createBuiltinSkills) if (hasDisabledSkills) { allSkills = allSkills.filter((s) => !options!.disabledSkills!.has(s.name)) } else { cachedSkillsByProvider.set(cacheKey, allSkills) } return allSkills } async function extractSkillTemplate(skill: LoadedSkill): Promise { if (skill.path) { const content = readFileSync(skill.path, "utf-8") const { body } = parseFrontmatter(content) return body.trim() } return skill.definition.template || "" } export { clearSkillCache, getAllSkills, extractSkillTemplate } export function injectGitMasterConfig(template: string, config?: GitMasterConfig): string { const commitFooter = config?.commit_footer ?? true const includeCoAuthoredBy = config?.include_co_authored_by ?? true if (!commitFooter && !includeCoAuthoredBy) { return template } const sections: string[] = [] sections.push(`### 5.5 Commit Footer & Co-Author`) sections.push(``) sections.push(`Add Sisyphus attribution to EVERY commit:`) sections.push(``) if (commitFooter) { const footerText = typeof commitFooter === "string" ? commitFooter : "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" sections.push(`1. **Footer in commit body:**`) sections.push("```") sections.push(footerText) sections.push("```") sections.push(``) } if (includeCoAuthoredBy) { sections.push(`${commitFooter ? "2" : "1"}. **Co-authored-by trailer:**`) sections.push("```") sections.push(`Co-authored-by: Sisyphus `) sections.push("```") sections.push(``) } if (commitFooter && includeCoAuthoredBy) { const footerText = typeof commitFooter === "string" ? commitFooter : "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" sections.push(`**Example (both enabled):**`) sections.push("```bash") sections.push(`git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus "`) sections.push("```") } else if (commitFooter) { const footerText = typeof commitFooter === "string" ? commitFooter : "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" sections.push(`**Example:**`) sections.push("```bash") sections.push(`git commit -m "{Commit Message}" -m "${footerText}"`) sections.push("```") } else if (includeCoAuthoredBy) { sections.push(`**Example:**`) sections.push("```bash") sections.push(`git commit -m "{Commit Message}" -m "Co-authored-by: Sisyphus "`) sections.push("```") } const injection = sections.join("\n") const insertionPoint = template.indexOf("```\n") if (insertionPoint !== -1) { return template.slice(0, insertionPoint) + "```\n\n" + injection + "\n" + template.slice(insertionPoint + "```\n".length) } return template + "\n\n" + injection } export function resolveSkillContent(skillName: string, options?: SkillResolutionOptions): string | null { const skills = createBuiltinSkills({ browserProvider: options?.browserProvider, disabledSkills: options?.disabledSkills, }) const skill = skills.find((s) => s.name === skillName) if (!skill) return null if (skillName === "git-master") { return injectGitMasterConfig(skill.template, options?.gitMasterConfig) } return skill.template } export function resolveMultipleSkills(skillNames: string[], options?: SkillResolutionOptions): { resolved: Map notFound: string[] } { const skills = createBuiltinSkills({ browserProvider: options?.browserProvider, disabledSkills: options?.disabledSkills, }) const skillMap = new Map(skills.map((s) => [s.name, s.template])) const resolved = new Map() const notFound: string[] = [] for (const name of skillNames) { const template = skillMap.get(name) if (template) { if (name === "git-master") { resolved.set(name, injectGitMasterConfig(template, options?.gitMasterConfig)) } else { resolved.set(name, template) } } else { notFound.push(name) } } return { resolved, notFound } } export async function resolveSkillContentAsync( skillName: string, options?: SkillResolutionOptions ): Promise { const allSkills = await getAllSkills(options) const skill = allSkills.find((s) => s.name === skillName) if (!skill) return null const template = await extractSkillTemplate(skill) if (skillName === "git-master") { return injectGitMasterConfig(template, options?.gitMasterConfig) } return template } export async function resolveMultipleSkillsAsync( skillNames: string[], options?: SkillResolutionOptions ): Promise<{ resolved: Map notFound: string[] }> { const allSkills = await getAllSkills(options) const skillMap = new Map() for (const skill of allSkills) { skillMap.set(skill.name, skill) } const resolved = new Map() const notFound: string[] = [] for (const name of skillNames) { const skill = skillMap.get(name) if (skill) { const template = await extractSkillTemplate(skill) if (name === "git-master") { resolved.set(name, injectGitMasterConfig(template, options?.gitMasterConfig)) } else { resolved.set(name, template) } } else { notFound.push(name) } } return { resolved, notFound } }