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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user