From b1f36d61a822231076cae500791fb6427e9fbe4e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 4 Jan 2026 21:43:28 +0900 Subject: [PATCH] perf(skill): implement lazy content loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LazyContentLoader interface to LoadedSkill type - Defer skill body loading until first use - Cache loaded content for subsequent calls - Reduce startup time by not reading full file contents 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/features/opencode-skill-loader/loader.ts | 41 ++++++++++++++++---- src/features/opencode-skill-loader/types.ts | 7 ++++ src/tools/skill/tools.ts | 10 ++++- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts index 9934b5c..9e2eb64 100644 --- a/src/features/opencode-skill-loader/loader.ts +++ b/src/features/opencode-skill-loader/loader.ts @@ -8,7 +8,7 @@ import { sanitizeModelField } from "../../shared/model-sanitizer" import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils" import { getClaudeConfigDir } from "../../shared" import type { CommandDefinition } from "../claude-code-command-loader/types" -import type { SkillScope, SkillMetadata, LoadedSkill } from "./types" +import type { SkillScope, SkillMetadata, LoadedSkill, LazyContentLoader } from "./types" import type { SkillMcpConfig } from "../skill-mcp-manager/types" function parseSkillMcpConfigFromFrontmatter(content: string): SkillMcpConfig | undefined { @@ -67,7 +67,7 @@ function loadSkillFromPath( ): LoadedSkill | null { try { const content = readFileSync(skillPath, "utf-8") - const { data, body } = parseFrontmatter(content) + const { data } = parseFrontmatter(content) const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp @@ -77,7 +77,15 @@ function loadSkillFromPath( const isOpencodeSource = scope === "opencode" || scope === "opencode-project" const formattedDescription = `(${scope} - Skill) ${originalDescription}` - const wrappedTemplate = ` + // Lazy content loader - only loads template on first use + const lazyContent: LazyContentLoader = { + loaded: false, + content: undefined, + load: async () => { + if (!lazyContent.loaded) { + const fileContent = await fs.readFile(skillPath, "utf-8") + const { body } = parseFrontmatter(fileContent) + lazyContent.content = ` Base directory for this skill: ${resolvedPath}/ File references (@path) in this skill are relative to this directory. @@ -87,11 +95,16 @@ ${body.trim()} $ARGUMENTS ` + lazyContent.loaded = true + } + return lazyContent.content! + }, + } const definition: CommandDefinition = { name: skillName, description: formattedDescription, - template: wrappedTemplate, + template: "", // Empty at startup, loaded lazily model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), agent: data.agent, subtask: data.subtask, @@ -109,6 +122,7 @@ $ARGUMENTS metadata: data.metadata, allowedTools: parseAllowedTools(data["allowed-tools"]), mcpConfig, + lazyContent, } } catch { return null @@ -123,7 +137,7 @@ async function loadSkillFromPathAsync( ): Promise { try { const content = await fs.readFile(skillPath, "utf-8") - const { data, body } = parseFrontmatter(content) + const { data } = parseFrontmatter(content) const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp @@ -133,7 +147,14 @@ async function loadSkillFromPathAsync( const isOpencodeSource = scope === "opencode" || scope === "opencode-project" const formattedDescription = `(${scope} - Skill) ${originalDescription}` - const wrappedTemplate = ` + const lazyContent: LazyContentLoader = { + loaded: false, + content: undefined, + load: async () => { + if (!lazyContent.loaded) { + const fileContent = await fs.readFile(skillPath, "utf-8") + const { body } = parseFrontmatter(fileContent) + lazyContent.content = ` Base directory for this skill: ${resolvedPath}/ File references (@path) in this skill are relative to this directory. @@ -143,11 +164,16 @@ ${body.trim()} $ARGUMENTS ` + lazyContent.loaded = true + } + return lazyContent.content! + }, + } const definition: CommandDefinition = { name: skillName, description: formattedDescription, - template: wrappedTemplate, + template: "", model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), agent: data.agent, subtask: data.subtask, @@ -165,6 +191,7 @@ $ARGUMENTS metadata: data.metadata, allowedTools: parseAllowedTools(data["allowed-tools"]), mcpConfig, + lazyContent, } } catch { return null diff --git a/src/features/opencode-skill-loader/types.ts b/src/features/opencode-skill-loader/types.ts index fe74dfa..18d9bc3 100644 --- a/src/features/opencode-skill-loader/types.ts +++ b/src/features/opencode-skill-loader/types.ts @@ -17,6 +17,12 @@ export interface SkillMetadata { mcp?: SkillMcpConfig } +export interface LazyContentLoader { + loaded: boolean + content?: string + load: () => Promise +} + export interface LoadedSkill { name: string path?: string @@ -28,4 +34,5 @@ export interface LoadedSkill { metadata?: Record allowedTools?: string[] mcpConfig?: SkillMcpConfig + lazyContent?: LazyContentLoader } diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index c5d6e85..50868c3 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -40,7 +40,13 @@ function formatSkillsXml(skills: SkillInfo[]): string { return `\n\n\n${skillsXml}\n` } -function extractSkillBody(skill: LoadedSkill): string { +async function extractSkillBody(skill: LoadedSkill): Promise { + if (skill.lazyContent) { + const fullTemplate = await skill.lazyContent.load() + const templateMatch = fullTemplate.match(/([\s\S]*?)<\/skill-instruction>/) + return templateMatch ? templateMatch[1].trim() : fullTemplate + } + if (skill.path) { const content = readFileSync(skill.path, "utf-8") const { body } = parseFrontmatter(content) @@ -145,7 +151,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`) } - const body = extractSkillBody(skill) + const body = await extractSkillBody(skill) const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd() const output = [