( filePath: string, pluginName: string, namespace: string[], sourceName: string, pluginPath: string, pluginManifest: PluginManifest, loadedPaths: Set<string>, )
| 63 | } |
| 64 | |
| 65 | async function loadAgentFromFile( |
| 66 | filePath: string, |
| 67 | pluginName: string, |
| 68 | namespace: string[], |
| 69 | sourceName: string, |
| 70 | pluginPath: string, |
| 71 | pluginManifest: PluginManifest, |
| 72 | loadedPaths: Set<string>, |
| 73 | ): Promise<AgentDefinition | null> { |
| 74 | const fs = getFsImplementation() |
| 75 | if (isDuplicatePath(fs, filePath, loadedPaths)) { |
| 76 | return null |
| 77 | } |
| 78 | try { |
| 79 | const content = await fs.readFile(filePath, { encoding: 'utf-8' }) |
| 80 | const { frontmatter, content: markdownContent } = parseFrontmatter( |
| 81 | content, |
| 82 | filePath, |
| 83 | ) |
| 84 | |
| 85 | const baseAgentName = |
| 86 | (frontmatter.name as string) || basename(filePath).replace(/\.md$/, '') |
| 87 | |
| 88 | // Apply namespace prefixing like we do for commands |
| 89 | const nameParts = [pluginName, ...namespace, baseAgentName] |
| 90 | const agentType = nameParts.join(':') |
| 91 | |
| 92 | // Parse agent metadata from frontmatter |
| 93 | const whenToUse = |
| 94 | coerceDescriptionToString(frontmatter.description, agentType) ?? |
| 95 | coerceDescriptionToString(frontmatter['when-to-use'], agentType) ?? |
| 96 | `Agent from ${pluginName} plugin` |
| 97 | |
| 98 | let tools = parseAgentToolsFromFrontmatter(frontmatter.tools) |
| 99 | const skills = parseSlashCommandToolsFromFrontmatter(frontmatter.skills) |
| 100 | const color = frontmatter.color as AgentColorName | undefined |
| 101 | const modelRaw = frontmatter.model |
| 102 | let model: string | undefined |
| 103 | if (typeof modelRaw === 'string' && modelRaw.trim().length > 0) { |
| 104 | const trimmed = modelRaw.trim() |
| 105 | model = trimmed.toLowerCase() === 'inherit' ? 'inherit' : trimmed |
| 106 | } |
| 107 | const backgroundRaw = frontmatter.background |
| 108 | const background = |
| 109 | backgroundRaw === 'true' || backgroundRaw === true ? true : undefined |
| 110 | // Substitute ${CLAUDE_PLUGIN_ROOT} so agents can reference bundled files, |
| 111 | // and ${user_config.X} (non-sensitive only) so they can embed configured |
| 112 | // usernames, endpoints, etc. Sensitive refs resolve to a placeholder. |
| 113 | let systemPrompt = substitutePluginVariables(markdownContent.trim(), { |
| 114 | path: pluginPath, |
| 115 | source: sourceName, |
| 116 | }) |
| 117 | if (pluginManifest.userConfig) { |
| 118 | systemPrompt = substituteUserConfigInContent( |
| 119 | systemPrompt, |
| 120 | loadPluginOptions(sourceName), |
| 121 | pluginManifest.userConfig, |
| 122 | ) |
no test coverage detected