perf(skill): implement lazy content loading

- 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)
This commit is contained in:
YeonGyu-Kim
2026-01-04 21:43:28 +09:00
parent 97e51c42dc
commit b1f36d61a8
3 changed files with 49 additions and 9 deletions

View File

@@ -8,7 +8,7 @@ import { sanitizeModelField } from "../../shared/model-sanitizer"
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils" import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared" import { getClaudeConfigDir } from "../../shared"
import type { CommandDefinition } from "../claude-code-command-loader/types" 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" import type { SkillMcpConfig } from "../skill-mcp-manager/types"
function parseSkillMcpConfigFromFrontmatter(content: string): SkillMcpConfig | undefined { function parseSkillMcpConfigFromFrontmatter(content: string): SkillMcpConfig | undefined {
@@ -67,7 +67,7 @@ function loadSkillFromPath(
): LoadedSkill | null { ): LoadedSkill | null {
try { try {
const content = readFileSync(skillPath, "utf-8") const content = readFileSync(skillPath, "utf-8")
const { data, body } = parseFrontmatter<SkillMetadata>(content) const { data } = parseFrontmatter<SkillMetadata>(content)
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp const mcpConfig = mcpJsonMcp || frontmatterMcp
@@ -77,7 +77,15 @@ function loadSkillFromPath(
const isOpencodeSource = scope === "opencode" || scope === "opencode-project" const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}` const formattedDescription = `(${scope} - Skill) ${originalDescription}`
const wrappedTemplate = `<skill-instruction> // 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<SkillMetadata>(fileContent)
lazyContent.content = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/ Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory. File references (@path) in this skill are relative to this directory.
@@ -87,11 +95,16 @@ ${body.trim()}
<user-request> <user-request>
$ARGUMENTS $ARGUMENTS
</user-request>` </user-request>`
lazyContent.loaded = true
}
return lazyContent.content!
},
}
const definition: CommandDefinition = { const definition: CommandDefinition = {
name: skillName, name: skillName,
description: formattedDescription, description: formattedDescription,
template: wrappedTemplate, template: "", // Empty at startup, loaded lazily
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
agent: data.agent, agent: data.agent,
subtask: data.subtask, subtask: data.subtask,
@@ -109,6 +122,7 @@ $ARGUMENTS
metadata: data.metadata, metadata: data.metadata,
allowedTools: parseAllowedTools(data["allowed-tools"]), allowedTools: parseAllowedTools(data["allowed-tools"]),
mcpConfig, mcpConfig,
lazyContent,
} }
} catch { } catch {
return null return null
@@ -123,7 +137,7 @@ async function loadSkillFromPathAsync(
): Promise<LoadedSkill | null> { ): Promise<LoadedSkill | null> {
try { try {
const content = await fs.readFile(skillPath, "utf-8") const content = await fs.readFile(skillPath, "utf-8")
const { data, body } = parseFrontmatter<SkillMetadata>(content) const { data } = parseFrontmatter<SkillMetadata>(content)
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp const mcpConfig = mcpJsonMcp || frontmatterMcp
@@ -133,7 +147,14 @@ async function loadSkillFromPathAsync(
const isOpencodeSource = scope === "opencode" || scope === "opencode-project" const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}` const formattedDescription = `(${scope} - Skill) ${originalDescription}`
const wrappedTemplate = `<skill-instruction> const lazyContent: LazyContentLoader = {
loaded: false,
content: undefined,
load: async () => {
if (!lazyContent.loaded) {
const fileContent = await fs.readFile(skillPath, "utf-8")
const { body } = parseFrontmatter<SkillMetadata>(fileContent)
lazyContent.content = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/ Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory. File references (@path) in this skill are relative to this directory.
@@ -143,11 +164,16 @@ ${body.trim()}
<user-request> <user-request>
$ARGUMENTS $ARGUMENTS
</user-request>` </user-request>`
lazyContent.loaded = true
}
return lazyContent.content!
},
}
const definition: CommandDefinition = { const definition: CommandDefinition = {
name: skillName, name: skillName,
description: formattedDescription, description: formattedDescription,
template: wrappedTemplate, template: "",
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
agent: data.agent, agent: data.agent,
subtask: data.subtask, subtask: data.subtask,
@@ -165,6 +191,7 @@ $ARGUMENTS
metadata: data.metadata, metadata: data.metadata,
allowedTools: parseAllowedTools(data["allowed-tools"]), allowedTools: parseAllowedTools(data["allowed-tools"]),
mcpConfig, mcpConfig,
lazyContent,
} }
} catch { } catch {
return null return null

View File

@@ -17,6 +17,12 @@ export interface SkillMetadata {
mcp?: SkillMcpConfig mcp?: SkillMcpConfig
} }
export interface LazyContentLoader {
loaded: boolean
content?: string
load: () => Promise<string>
}
export interface LoadedSkill { export interface LoadedSkill {
name: string name: string
path?: string path?: string
@@ -28,4 +34,5 @@ export interface LoadedSkill {
metadata?: Record<string, string> metadata?: Record<string, string>
allowedTools?: string[] allowedTools?: string[]
mcpConfig?: SkillMcpConfig mcpConfig?: SkillMcpConfig
lazyContent?: LazyContentLoader
} }

View File

@@ -40,7 +40,13 @@ function formatSkillsXml(skills: SkillInfo[]): string {
return `\n\n<available_skills>\n${skillsXml}\n</available_skills>` return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`
} }
function extractSkillBody(skill: LoadedSkill): string { async function extractSkillBody(skill: LoadedSkill): Promise<string> {
if (skill.lazyContent) {
const fullTemplate = await skill.lazyContent.load()
const templateMatch = fullTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/)
return templateMatch ? templateMatch[1].trim() : fullTemplate
}
if (skill.path) { if (skill.path) {
const content = readFileSync(skill.path, "utf-8") const content = readFileSync(skill.path, "utf-8")
const { body } = parseFrontmatter(content) 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"}`) 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 dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd()
const output = [ const output = [