Files
oh-my-opencode/src/features/opencode-skill-loader/loaded-skill-from-path.ts
T
YeonGyu-Kim 62c60ae9d8 fix: numeric skill names, ultrawork missing run_in_background, ZWSP agent lookups
- #3354: Coerce data.name to String in loadSkillFromPath/loadSkillFromPathAsync
  to prevent crash when YAML parses numeric skill names (e.g., name: 12306)

- #3416: Add required run_in_background parameter to all task() examples in
  ultrawork prompts (default, gpt, gemini, planner) to match tool schema

- #3379/#3417/#3418/#3337/#3335: Strip ZWSP (U+200B) before agent name
  comparisons in agent-tool-restrictions, sync-prompt-sender, tool-execute-after,
  tool-execute-before, oracle-verification-detector, call-omo-agent,
  recovery-prompt-config, and agent-variant to prevent ZWSP-prefixed display
  names from breaking exact-match lookups
2026-04-15 10:46:41 +09:00

74 lines
2.8 KiB
TypeScript

import { promises as fs } from "fs"
import { basename } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
import type { CommandDefinition } from "../claude-code-command-loader/types"
import { parseAllowedTools } from "./allowed-tools-parser"
import { loadMcpJsonFromDir, parseSkillMcpConfigFromFrontmatter } from "./skill-mcp-config"
import type { SkillScope, SkillMetadata, LoadedSkill, LazyContentLoader } from "./types"
export async function loadSkillFromPath(options: {
skillPath: string
resolvedPath: string
defaultName: string
scope: SkillScope
namePrefix?: string
}): Promise<LoadedSkill | null> {
const namePrefix = options.namePrefix ?? ""
try {
const content = await fs.readFile(options.skillPath, "utf-8")
const { data, body } = parseFrontmatter<SkillMetadata>(content)
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = await loadMcpJsonFromDir(options.resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
const baseName = String(data.name || options.defaultName)
const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName
const originalDescription = data.description || ""
const isOpencodeSource = options.scope === "opencode" || options.scope === "opencode-project"
const formattedDescription = `(${options.scope} - Skill) ${originalDescription}`
const resolvedBody = resolveSkillPathReferences(body.trim(), options.resolvedPath)
const templateContent = `<skill-instruction>\nBase directory for this skill: ${options.resolvedPath}/\nFile references (@path) in this skill are relative to this directory.\n\n${resolvedBody}\n</skill-instruction>\n\n<user-request>\n$ARGUMENTS\n</user-request>`
const eagerLoader: LazyContentLoader = {
loaded: true,
content: templateContent,
load: async () => templateContent,
}
const definition: CommandDefinition = {
name: skillName,
description: formattedDescription,
template: templateContent,
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
agent: data.agent,
subtask: data.subtask,
argumentHint: data["argument-hint"],
}
return {
name: skillName,
path: options.skillPath,
resolvedPath: options.resolvedPath,
definition,
scope: options.scope,
license: data.license,
compatibility: data.compatibility,
metadata: data.metadata,
allowedTools: parseAllowedTools(data["allowed-tools"]),
mcpConfig,
lazyContent: eagerLoader,
}
} catch {
return null
}
}
export function inferSkillNameFromFileName(filePath: string): string {
return basename(filePath, ".md")
}