Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
31 kB · 946 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947import memoize from 'lodash-es/memoize.js'import { basename, dirname, join } from 'path'import { getInlinePlugins, getSessionId } from '../../bootstrap/state.js'import type { Command } from '../../types/command.js'import { getPluginErrorMessage } from '../../types/plugin.js'import { parseArgumentNames, substituteArguments,} from '../argumentSubstitution.js'import { logForDebugging } from '../debug.js'import { EFFORT_LEVELS, parseEffortValue } from '../effort.js'import { isBareMode } from '../envUtils.js'import { isENOENT } from '../errors.js'import { coerceDescriptionToString, type FrontmatterData, parseBooleanFrontmatter, parseFrontmatter, parseShellFrontmatter,} from '../frontmatterParser.js'import { getFsImplementation, isDuplicatePath } from '../fsOperations.js'import { extractDescriptionFromMarkdown, parseSlashCommandToolsFromFrontmatter,} from '../markdownConfigLoader.js'import { parseUserSpecifiedModel } from '../model/model.js'import { executeShellCommandsInPrompt } from '../promptShellExecution.js'import { loadAllPluginsCacheOnly } from './pluginLoader.js'import { loadPluginOptions, substitutePluginVariables, substituteUserConfigInContent,} from './pluginOptionsStorage.js'import type { CommandMetadata, PluginManifest } from './schemas.js'import { walkPluginMarkdown } from './walkPluginMarkdown.js'
// Similar to MarkdownFile but for plugin sourcestype PluginMarkdownFile = { filePath: string baseDir: string frontmatter: FrontmatterData content: string}
// Configuration for loading commands or skillstype LoadConfig = { isSkillMode: boolean // true when loading from skills/ directory}
/** * Check if a file path is a skill file (SKILL.md) */function isSkillFile(filePath: string): boolean { return /^skill\.md$/i.test(basename(filePath))}
/** * Get command name from file path, handling both regular files and skills */function getCommandNameFromFile( filePath: string, baseDir: string, pluginName: string,): string { const isSkill = isSkillFile(filePath)
if (isSkill) { // For skills, use the parent directory name const skillDirectory = dirname(filePath) const parentOfSkillDir = dirname(skillDirectory) const commandBaseName = basename(skillDirectory)
// Build namespace from parent of skill directory const relativePath = parentOfSkillDir.startsWith(baseDir) ? parentOfSkillDir.slice(baseDir.length).replace(/^\//, '') : '' const namespace = relativePath ? relativePath.split('/').join(':') : ''
return namespace ? `${pluginName}:${namespace}:${commandBaseName}` : `${pluginName}:${commandBaseName}` } else { // For regular files, use filename without .md const fileDirectory = dirname(filePath) const commandBaseName = basename(filePath).replace(/\.md$/, '')
// Build namespace from file directory const relativePath = fileDirectory.startsWith(baseDir) ? fileDirectory.slice(baseDir.length).replace(/^\//, '') : '' const namespace = relativePath ? relativePath.split('/').join(':') : ''
return namespace ? `${pluginName}:${namespace}:${commandBaseName}` : `${pluginName}:${commandBaseName}` }}
/** * Recursively collects all markdown files from a directory */async function collectMarkdownFiles( dirPath: string, baseDir: string, loadedPaths: Set<string>,): Promise<PluginMarkdownFile[]> { const files: PluginMarkdownFile[] = [] const fs = getFsImplementation()
await walkPluginMarkdown( dirPath, async fullPath => { if (isDuplicatePath(fs, fullPath, loadedPaths)) return const content = await fs.readFile(fullPath, { encoding: 'utf-8' }) const { frontmatter, content: markdownContent } = parseFrontmatter( content, fullPath, ) files.push({ filePath: fullPath, baseDir, frontmatter, content: markdownContent, }) }, { stopAtSkillDir: true, logLabel: 'commands' }, )
return files}
/** * Transforms plugin markdown files to handle skill directories */function transformPluginSkillFiles( files: PluginMarkdownFile[],): PluginMarkdownFile[] { const filesByDir = new Map<string, PluginMarkdownFile[]>()
for (const file of files) { const dir = dirname(file.filePath) const dirFiles = filesByDir.get(dir) ?? [] dirFiles.push(file) filesByDir.set(dir, dirFiles) }
const result: PluginMarkdownFile[] = []
for (const [dir, dirFiles] of filesByDir) { const skillFiles = dirFiles.filter(f => isSkillFile(f.filePath)) if (skillFiles.length > 0) { // Use the first skill file if multiple exist const skillFile = skillFiles[0]! if (skillFiles.length > 1) { logForDebugging( `Multiple skill files found in ${dir}, using ${basename(skillFile.filePath)}`, ) } // Directory has a skill - only include the skill file result.push(skillFile) } else { result.push(...dirFiles) } }
return result}
async function loadCommandsFromDirectory( commandsPath: string, pluginName: string, sourceName: string, pluginManifest: PluginManifest, pluginPath: string, config: LoadConfig = { isSkillMode: false }, loadedPaths: Set<string> = new Set(),): Promise<Command[]> { // Collect all markdown files const markdownFiles = await collectMarkdownFiles( commandsPath, commandsPath, loadedPaths, )
// Apply skill transformation const processedFiles = transformPluginSkillFiles(markdownFiles)
// Convert to commands const commands: Command[] = [] for (const file of processedFiles) { const commandName = getCommandNameFromFile( file.filePath, file.baseDir, pluginName, )
const command = createPluginCommand( commandName, file, sourceName, pluginManifest, pluginPath, isSkillFile(file.filePath), config, )
if (command) { commands.push(command) } }
return commands}
/** * Create a Command from a plugin markdown file */function createPluginCommand( commandName: string, file: PluginMarkdownFile, sourceName: string, pluginManifest: PluginManifest, pluginPath: string, isSkill: boolean, config: LoadConfig = { isSkillMode: false },): Command | null { try { const { frontmatter, content } = file
const validatedDescription = coerceDescriptionToString( frontmatter.description, commandName, ) const description = validatedDescription ?? extractDescriptionFromMarkdown( content, isSkill ? 'Plugin skill' : 'Plugin command', )
// Substitute ${CLAUDE_PLUGIN_ROOT} in allowed-tools before parsing const rawAllowedTools = frontmatter['allowed-tools'] const substitutedAllowedTools = typeof rawAllowedTools === 'string' ? substitutePluginVariables(rawAllowedTools, { path: pluginPath, source: sourceName, }) : Array.isArray(rawAllowedTools) ? rawAllowedTools.map(tool => typeof tool === 'string' ? substitutePluginVariables(tool, { path: pluginPath, source: sourceName, }) : tool, ) : rawAllowedTools const allowedTools = parseSlashCommandToolsFromFrontmatter( substitutedAllowedTools, )
const argumentHint = frontmatter['argument-hint'] as string | undefined const argumentNames = parseArgumentNames( frontmatter.arguments as string | string[] | undefined, ) const whenToUse = frontmatter.when_to_use as string | undefined const version = frontmatter.version as string | undefined const displayName = frontmatter.name as string | undefined
// Handle model configuration, resolving aliases like 'haiku', 'sonnet', 'opus' const model = frontmatter.model === 'inherit' ? undefined : frontmatter.model ? parseUserSpecifiedModel(frontmatter.model as string) : undefined
const effortRaw = frontmatter['effort'] const effort = effortRaw !== undefined ? parseEffortValue(effortRaw) : undefined if (effortRaw !== undefined && effort === undefined) { logForDebugging( `Plugin command ${commandName} has invalid effort '${effortRaw}'. Valid options: ${EFFORT_LEVELS.join(', ')} or an integer`, ) }
const disableModelInvocation = parseBooleanFrontmatter( frontmatter['disable-model-invocation'], )
const userInvocableValue = frontmatter['user-invocable'] const userInvocable = userInvocableValue === undefined ? true : parseBooleanFrontmatter(userInvocableValue)
const shell = parseShellFrontmatter(frontmatter.shell, commandName)
return { type: 'prompt', name: commandName, description, hasUserSpecifiedDescription: validatedDescription !== null, allowedTools, argumentHint, argNames: argumentNames.length > 0 ? argumentNames : undefined, whenToUse, version, model, effort, disableModelInvocation, userInvocable, contentLength: content.length, source: 'plugin' as const, loadedFrom: isSkill || config.isSkillMode ? 'plugin' : undefined, pluginInfo: { pluginManifest, repository: sourceName, }, isHidden: !userInvocable, progressMessage: isSkill || config.isSkillMode ? 'loading' : 'running', userFacingName(): string { return displayName || commandName }, async getPromptForCommand(args, context) { // For skills from skills/ directory, include base directory let finalContent = config.isSkillMode ? `Base directory for this skill: ${dirname(file.filePath)}\n\n${content}` : content
finalContent = substituteArguments( finalContent, args, true, argumentNames, )
// Replace ${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_DATA} with their paths finalContent = substitutePluginVariables(finalContent, { path: pluginPath, source: sourceName, })
// Replace ${user_config.X} with saved option values. Sensitive keys // resolve to a descriptive placeholder instead — skill content goes to // the model prompt and we don't put secrets there. if (pluginManifest.userConfig) { finalContent = substituteUserConfigInContent( finalContent, loadPluginOptions(sourceName), pluginManifest.userConfig, ) }
// Replace ${CLAUDE_SKILL_DIR} with this specific skill's directory. // Distinct from ${CLAUDE_PLUGIN_ROOT}: a plugin can contain multiple // skills, so CLAUDE_PLUGIN_ROOT points to the plugin root while // CLAUDE_SKILL_DIR points to the individual skill's subdirectory. if (config.isSkillMode) { const rawSkillDir = dirname(file.filePath) const skillDir = process.platform === 'win32' ? rawSkillDir.replace(/\\/g, '/') : rawSkillDir finalContent = finalContent.replace( /\$\{CLAUDE_SKILL_DIR\}/g, skillDir, ) }
// Replace ${CLAUDE_SESSION_ID} with the current session ID finalContent = finalContent.replace( /\$\{CLAUDE_SESSION_ID\}/g, getSessionId(), )
finalContent = await executeShellCommandsInPrompt( finalContent, { ...context, getAppState() { const appState = context.getAppState() return { ...appState, toolPermissionContext: { ...appState.toolPermissionContext, alwaysAllowRules: { ...appState.toolPermissionContext.alwaysAllowRules, command: allowedTools, }, }, } }, }, `/${commandName}`, shell, )
return [{ type: 'text', text: finalContent }] }, } satisfies Command } catch (error) { logForDebugging( `Failed to create command from ${file.filePath}: ${error}`, { level: 'error', }, ) return null }}
export const getPluginCommands = memoize(async (): Promise<Command[]> => { // --bare: skip marketplace plugin auto-load. Explicit --plugin-dir still // works — getInlinePlugins() is set by main.tsx from --plugin-dir. // loadAllPluginsCacheOnly already short-circuits to inline-only when // inlinePlugins.length > 0. if (isBareMode() && getInlinePlugins().length === 0) { return [] } // Only load commands from enabled plugins const { enabled, errors } = await loadAllPluginsCacheOnly()
if (errors.length > 0) { logForDebugging( `Plugin loading errors: ${errors.map(e => getPluginErrorMessage(e)).join(', ')}`, ) }
// Process plugins in parallel; each plugin has its own loadedPaths scope const perPluginCommands = await Promise.all( enabled.map(async (plugin): Promise<Command[]> => { // Track loaded file paths to prevent duplicates within this plugin const loadedPaths = new Set<string>() const pluginCommands: Command[] = []
// Load commands from default commands directory if (plugin.commandsPath) { try { const commands = await loadCommandsFromDirectory( plugin.commandsPath, plugin.name, plugin.source, plugin.manifest, plugin.path, { isSkillMode: false }, loadedPaths, ) pluginCommands.push(...commands)
if (commands.length > 0) { logForDebugging( `Loaded ${commands.length} commands from plugin ${plugin.name} default directory`, ) } } catch (error) { logForDebugging( `Failed to load commands from plugin ${plugin.name} default directory: ${error}`, { level: 'error' }, ) } }
// Load commands from additional paths specified in manifest if (plugin.commandsPaths) { logForDebugging( `Plugin ${plugin.name} has commandsPaths: ${plugin.commandsPaths.join(', ')}`, ) // Process all commandsPaths in parallel. isDuplicatePath is synchronous // (check-and-add), so concurrent access to loadedPaths is safe. const pathResults = await Promise.all( plugin.commandsPaths.map(async (commandPath): Promise<Command[]> => { try { const fs = getFsImplementation() const stats = await fs.stat(commandPath) logForDebugging( `Checking commandPath ${commandPath} - isDirectory: ${stats.isDirectory()}, isFile: ${stats.isFile()}`, )
if (stats.isDirectory()) { // Load all .md files and skill directories from directory const commands = await loadCommandsFromDirectory( commandPath, plugin.name, plugin.source, plugin.manifest, plugin.path, { isSkillMode: false }, loadedPaths, )
if (commands.length > 0) { logForDebugging( `Loaded ${commands.length} commands from plugin ${plugin.name} custom path: ${commandPath}`, ) } else { logForDebugging( `Warning: No commands found in plugin ${plugin.name} custom directory: ${commandPath}. Expected .md files or SKILL.md in subdirectories.`, { level: 'warn' }, ) } return commands } else if (stats.isFile() && commandPath.endsWith('.md')) { if (isDuplicatePath(fs, commandPath, loadedPaths)) { return [] }
// Load single command file const content = await fs.readFile(commandPath, { encoding: 'utf-8', }) const { frontmatter, content: markdownContent } = parseFrontmatter(content, commandPath)
// Check if there's metadata for this command (object-mapping format) let commandName: string | undefined let metadataOverride: CommandMetadata | undefined
if (plugin.commandsMetadata) { // Find metadata by matching the command's absolute path to the metadata source // Convert metadata.source (relative to plugin root) to absolute path for comparison for (const [name, metadata] of Object.entries( plugin.commandsMetadata, )) { if (metadata.source) { const fullMetadataPath = join( plugin.path, metadata.source, ) if (commandPath === fullMetadataPath) { commandName = `${plugin.name}:${name}` metadataOverride = metadata break } } } }
// Fall back to filename-based naming if no metadata if (!commandName) { commandName = `${plugin.name}:${basename(commandPath).replace(/\.md$/, '')}` }
// Apply metadata overrides to frontmatter const finalFrontmatter = metadataOverride ? { ...frontmatter, ...(metadataOverride.description && { description: metadataOverride.description, }), ...(metadataOverride.argumentHint && { 'argument-hint': metadataOverride.argumentHint, }), ...(metadataOverride.model && { model: metadataOverride.model, }), ...(metadataOverride.allowedTools && { 'allowed-tools': metadataOverride.allowedTools.join(','), }), } : frontmatter
const file: PluginMarkdownFile = { filePath: commandPath, baseDir: dirname(commandPath), frontmatter: finalFrontmatter, content: markdownContent, }
const command = createPluginCommand( commandName, file, plugin.source, plugin.manifest, plugin.path, false, )
if (command) { logForDebugging( `Loaded command from plugin ${plugin.name} custom file: ${commandPath}${metadataOverride ? ' (with metadata override)' : ''}`, ) return [command] } } return [] } catch (error) { logForDebugging( `Failed to load commands from plugin ${plugin.name} custom path ${commandPath}: ${error}`, { level: 'error' }, ) return [] } }), ) for (const commands of pathResults) { pluginCommands.push(...commands) } }
// Load commands with inline content (no source file) // Note: Commands with source files were already loaded in the previous loop // when iterating through commandsPaths. This loop handles metadata entries // that specify inline content instead of file references. if (plugin.commandsMetadata) { for (const [name, metadata] of Object.entries( plugin.commandsMetadata, )) { // Only process entries with inline content (no source) if (metadata.content && !metadata.source) { try { // Parse inline content for frontmatter const { frontmatter, content: markdownContent } = parseFrontmatter( metadata.content, `<inline:${plugin.name}:${name}>`, )
// Apply metadata overrides to frontmatter const finalFrontmatter: FrontmatterData = { ...frontmatter, ...(metadata.description && { description: metadata.description, }), ...(metadata.argumentHint && { 'argument-hint': metadata.argumentHint, }), ...(metadata.model && { model: metadata.model, }), ...(metadata.allowedTools && { 'allowed-tools': metadata.allowedTools.join(','), }), }
const commandName = `${plugin.name}:${name}` const file: PluginMarkdownFile = { filePath: `<inline:${commandName}>`, // Virtual path for inline content baseDir: plugin.path, // Use plugin root as base directory frontmatter: finalFrontmatter, content: markdownContent, }
const command = createPluginCommand( commandName, file, plugin.source, plugin.manifest, plugin.path, false, )
if (command) { pluginCommands.push(command) logForDebugging( `Loaded inline content command from plugin ${plugin.name}: ${commandName}`, ) } } catch (error) { logForDebugging( `Failed to load inline content command ${name} from plugin ${plugin.name}: ${error}`, { level: 'error' }, ) } } } } return pluginCommands }), )
const allCommands = perPluginCommands.flat() logForDebugging(`Total plugin commands loaded: ${allCommands.length}`) return allCommands})
export function clearPluginCommandCache(): void { getPluginCommands.cache?.clear?.()}
/** * Loads skills from plugin skills directories * Skills are directories containing SKILL.md files */async function loadSkillsFromDirectory( skillsPath: string, pluginName: string, sourceName: string, pluginManifest: PluginManifest, pluginPath: string, loadedPaths: Set<string>,): Promise<Command[]> { const fs = getFsImplementation() const skills: Command[] = []
// First, check if skillsPath itself contains SKILL.md (direct skill directory) const directSkillPath = join(skillsPath, 'SKILL.md') let directSkillContent: string | null = null try { directSkillContent = await fs.readFile(directSkillPath, { encoding: 'utf-8', }) } catch (e: unknown) { if (!isENOENT(e)) { logForDebugging(`Failed to load skill from ${directSkillPath}: ${e}`, { level: 'error', }) return skills } // ENOENT: no direct SKILL.md, fall through to scan subdirectories }
if (directSkillContent !== null) { // This is a direct skill directory, load the skill from here if (isDuplicatePath(fs, directSkillPath, loadedPaths)) { return skills } try { const { frontmatter, content: markdownContent } = parseFrontmatter( directSkillContent, directSkillPath, )
const skillName = `${pluginName}:${basename(skillsPath)}`
const file: PluginMarkdownFile = { filePath: directSkillPath, baseDir: dirname(directSkillPath), frontmatter, content: markdownContent, }
const skill = createPluginCommand( skillName, file, sourceName, pluginManifest, pluginPath, true, // isSkill { isSkillMode: true }, // config )
if (skill) { skills.push(skill) } } catch (error) { logForDebugging( `Failed to load skill from ${directSkillPath}: ${error}`, { level: 'error', }, ) } return skills }
// Otherwise, scan for subdirectories containing SKILL.md files let entries try { entries = await fs.readdir(skillsPath) } catch (e: unknown) { if (!isENOENT(e)) { logForDebugging( `Failed to load skills from directory ${skillsPath}: ${e}`, { level: 'error' }, ) } return skills }
await Promise.all( entries.map(async entry => { // Accept both directories and symlinks (symlinks may point to skill directories) if (!entry.isDirectory() && !entry.isSymbolicLink()) { return }
const skillDirPath = join(skillsPath, entry.name) const skillFilePath = join(skillDirPath, 'SKILL.md')
// Try to read SKILL.md directly; skip if it doesn't exist let content: string try { content = await fs.readFile(skillFilePath, { encoding: 'utf-8' }) } catch (e: unknown) { if (!isENOENT(e)) { logForDebugging(`Failed to load skill from ${skillFilePath}: ${e}`, { level: 'error', }) } return }
if (isDuplicatePath(fs, skillFilePath, loadedPaths)) { return }
try { const { frontmatter, content: markdownContent } = parseFrontmatter( content, skillFilePath, )
const skillName = `${pluginName}:${entry.name}`
const file: PluginMarkdownFile = { filePath: skillFilePath, baseDir: dirname(skillFilePath), frontmatter, content: markdownContent, }
const skill = createPluginCommand( skillName, file, sourceName, pluginManifest, pluginPath, true, // isSkill { isSkillMode: true }, // config )
if (skill) { skills.push(skill) } } catch (error) { logForDebugging( `Failed to load skill from ${skillFilePath}: ${error}`, { level: 'error' }, ) } }), )
return skills}
export const getPluginSkills = memoize(async (): Promise<Command[]> => { // --bare: same gate as getPluginCommands above — honor explicit // --plugin-dir, skip marketplace auto-load. if (isBareMode() && getInlinePlugins().length === 0) { return [] } // Only load skills from enabled plugins const { enabled, errors } = await loadAllPluginsCacheOnly()
if (errors.length > 0) { logForDebugging( `Plugin loading errors: ${errors.map(e => getPluginErrorMessage(e)).join(', ')}`, ) }
logForDebugging( `getPluginSkills: Processing ${enabled.length} enabled plugins`, )
// Process plugins in parallel; each plugin has its own loadedPaths scope const perPluginSkills = await Promise.all( enabled.map(async (plugin): Promise<Command[]> => { // Track loaded file paths to prevent duplicates within this plugin const loadedPaths = new Set<string>() const pluginSkills: Command[] = []
logForDebugging( `Checking plugin ${plugin.name}: skillsPath=${plugin.skillsPath ? 'exists' : 'none'}, skillsPaths=${plugin.skillsPaths ? plugin.skillsPaths.length : 0} paths`, ) // Load skills from default skills directory if (plugin.skillsPath) { logForDebugging( `Attempting to load skills from plugin ${plugin.name} default skillsPath: ${plugin.skillsPath}`, ) try { const skills = await loadSkillsFromDirectory( plugin.skillsPath, plugin.name, plugin.source, plugin.manifest, plugin.path, loadedPaths, ) pluginSkills.push(...skills)
logForDebugging( `Loaded ${skills.length} skills from plugin ${plugin.name} default directory`, ) } catch (error) { logForDebugging( `Failed to load skills from plugin ${plugin.name} default directory: ${error}`, { level: 'error' }, ) } }
// Load skills from additional paths specified in manifest if (plugin.skillsPaths) { logForDebugging( `Attempting to load skills from plugin ${plugin.name} skillsPaths: ${plugin.skillsPaths.join(', ')}`, ) // Process all skillsPaths in parallel. isDuplicatePath is synchronous // (check-and-add), so concurrent access to loadedPaths is safe. const pathResults = await Promise.all( plugin.skillsPaths.map(async (skillPath): Promise<Command[]> => { try { logForDebugging( `Loading from skillPath: ${skillPath} for plugin ${plugin.name}`, ) const skills = await loadSkillsFromDirectory( skillPath, plugin.name, plugin.source, plugin.manifest, plugin.path, loadedPaths, )
logForDebugging( `Loaded ${skills.length} skills from plugin ${plugin.name} custom path: ${skillPath}`, ) return skills } catch (error) { logForDebugging( `Failed to load skills from plugin ${plugin.name} custom path ${skillPath}: ${error}`, { level: 'error' }, ) return [] } }), ) for (const skills of pathResults) { pluginSkills.push(...skills) } } return pluginSkills }), )
const allSkills = perPluginSkills.flat() logForDebugging(`Total plugin skills loaded: ${allSkills.length}`) return allSkills})
export function clearPluginSkillsCache(): void { getPluginSkills.cache?.clear?.()}