source dump of claude code
at main 348 lines 12 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import { basename } from 'path' 3import { isAutoMemoryEnabled } from '../../memdir/paths.js' 4import type { AgentColorName } from '../../tools/AgentTool/agentColorManager.js' 5import { 6 type AgentMemoryScope, 7 loadAgentMemoryPrompt, 8} from '../../tools/AgentTool/agentMemory.js' 9import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' 10import { FILE_EDIT_TOOL_NAME } from '../../tools/FileEditTool/constants.js' 11import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js' 12import { FILE_WRITE_TOOL_NAME } from '../../tools/FileWriteTool/prompt.js' 13import { getPluginErrorMessage } from '../../types/plugin.js' 14import { logForDebugging } from '../debug.js' 15import { EFFORT_LEVELS, parseEffortValue } from '../effort.js' 16import { 17 coerceDescriptionToString, 18 parseFrontmatter, 19 parsePositiveIntFromFrontmatter, 20} from '../frontmatterParser.js' 21import { getFsImplementation, isDuplicatePath } from '../fsOperations.js' 22import { 23 parseAgentToolsFromFrontmatter, 24 parseSlashCommandToolsFromFrontmatter, 25} from '../markdownConfigLoader.js' 26import { loadAllPluginsCacheOnly } from './pluginLoader.js' 27import { 28 loadPluginOptions, 29 substitutePluginVariables, 30 substituteUserConfigInContent, 31} from './pluginOptionsStorage.js' 32import type { PluginManifest } from './schemas.js' 33import { walkPluginMarkdown } from './walkPluginMarkdown.js' 34 35const VALID_MEMORY_SCOPES: AgentMemoryScope[] = ['user', 'project', 'local'] 36 37async function loadAgentsFromDirectory( 38 agentsPath: string, 39 pluginName: string, 40 sourceName: string, 41 pluginPath: string, 42 pluginManifest: PluginManifest, 43 loadedPaths: Set<string>, 44): Promise<AgentDefinition[]> { 45 const agents: AgentDefinition[] = [] 46 await walkPluginMarkdown( 47 agentsPath, 48 async (fullPath, namespace) => { 49 const agent = await loadAgentFromFile( 50 fullPath, 51 pluginName, 52 namespace, 53 sourceName, 54 pluginPath, 55 pluginManifest, 56 loadedPaths, 57 ) 58 if (agent) agents.push(agent) 59 }, 60 { logLabel: 'agents' }, 61 ) 62 return agents 63} 64 65async 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 ) 123 } 124 125 // Parse memory scope 126 const memoryRaw = frontmatter.memory as string | undefined 127 let memory: AgentMemoryScope | undefined 128 if (memoryRaw !== undefined) { 129 if (VALID_MEMORY_SCOPES.includes(memoryRaw as AgentMemoryScope)) { 130 memory = memoryRaw as AgentMemoryScope 131 } else { 132 logForDebugging( 133 `Plugin agent file ${filePath} has invalid memory value '${memoryRaw}'. Valid options: ${VALID_MEMORY_SCOPES.join(', ')}`, 134 ) 135 } 136 } 137 138 // Parse isolation mode 139 const isolationRaw = frontmatter.isolation as string | undefined 140 const isolation = 141 isolationRaw === 'worktree' ? ('worktree' as const) : undefined 142 143 // Parse effort (string level or integer) 144 const effortRaw = frontmatter.effort 145 const effort = 146 effortRaw !== undefined ? parseEffortValue(effortRaw) : undefined 147 if (effortRaw !== undefined && effort === undefined) { 148 logForDebugging( 149 `Plugin agent file ${filePath} has invalid effort '${effortRaw}'. Valid options: ${EFFORT_LEVELS.join(', ')} or an integer`, 150 ) 151 } 152 153 // permissionMode, hooks, and mcpServers are intentionally NOT parsed for 154 // plugin agents. Plugins are third-party marketplace code; these fields 155 // escalate what the agent can do beyond what the user approved at install 156 // time. For this level of control, define the agent in .claude/agents/ 157 // where the user explicitly wrote the frontmatter. (Note: plugins can 158 // still ship hooks and MCP servers at the manifest level — that's the 159 // install-time trust boundary. Per-agent declarations would let a single 160 // agent file buried in agents/ silently add them.) See PR #22558 review. 161 for (const field of ['permissionMode', 'hooks', 'mcpServers'] as const) { 162 if (frontmatter[field] !== undefined) { 163 logForDebugging( 164 `Plugin agent file ${filePath} sets ${field}, which is ignored for plugin agents. Use .claude/agents/ for this level of control.`, 165 { level: 'warn' }, 166 ) 167 } 168 } 169 170 // Parse maxTurns 171 const maxTurnsRaw = frontmatter.maxTurns 172 const maxTurns = parsePositiveIntFromFrontmatter(maxTurnsRaw) 173 if (maxTurnsRaw !== undefined && maxTurns === undefined) { 174 logForDebugging( 175 `Plugin agent file ${filePath} has invalid maxTurns '${maxTurnsRaw}'. Must be a positive integer.`, 176 ) 177 } 178 179 // Parse disallowedTools 180 const disallowedTools = 181 frontmatter.disallowedTools !== undefined 182 ? parseAgentToolsFromFrontmatter(frontmatter.disallowedTools) 183 : undefined 184 185 // If memory is enabled, inject Write/Edit/Read tools for memory access 186 if (isAutoMemoryEnabled() && memory && tools !== undefined) { 187 const toolSet = new Set(tools) 188 for (const tool of [ 189 FILE_WRITE_TOOL_NAME, 190 FILE_EDIT_TOOL_NAME, 191 FILE_READ_TOOL_NAME, 192 ]) { 193 if (!toolSet.has(tool)) { 194 tools = [...tools, tool] 195 } 196 } 197 } 198 199 return { 200 agentType, 201 whenToUse, 202 tools, 203 ...(disallowedTools !== undefined ? { disallowedTools } : {}), 204 ...(skills !== undefined ? { skills } : {}), 205 getSystemPrompt: () => { 206 if (isAutoMemoryEnabled() && memory) { 207 const memoryPrompt = loadAgentMemoryPrompt(agentType, memory) 208 return systemPrompt + '\n\n' + memoryPrompt 209 } 210 return systemPrompt 211 }, 212 source: 'plugin' as const, 213 color, 214 model, 215 filename: baseAgentName, 216 plugin: sourceName, 217 ...(background ? { background } : {}), 218 ...(memory ? { memory } : {}), 219 ...(isolation ? { isolation } : {}), 220 ...(effort !== undefined ? { effort } : {}), 221 ...(maxTurns !== undefined ? { maxTurns } : {}), 222 } as AgentDefinition 223 } catch (error) { 224 logForDebugging(`Failed to load agent from ${filePath}: ${error}`, { 225 level: 'error', 226 }) 227 return null 228 } 229} 230 231export const loadPluginAgents = memoize( 232 async (): Promise<AgentDefinition[]> => { 233 // Only load agents from enabled plugins 234 const { enabled, errors } = await loadAllPluginsCacheOnly() 235 236 if (errors.length > 0) { 237 logForDebugging( 238 `Plugin loading errors: ${errors.map(e => getPluginErrorMessage(e)).join(', ')}`, 239 ) 240 } 241 242 // Process plugins in parallel; each plugin has its own loadedPaths scope 243 const perPluginAgents = await Promise.all( 244 enabled.map(async (plugin): Promise<AgentDefinition[]> => { 245 // Track loaded file paths to prevent duplicates within this plugin 246 const loadedPaths = new Set<string>() 247 const pluginAgents: AgentDefinition[] = [] 248 249 // Load agents from default agents directory 250 if (plugin.agentsPath) { 251 try { 252 const agents = await loadAgentsFromDirectory( 253 plugin.agentsPath, 254 plugin.name, 255 plugin.source, 256 plugin.path, 257 plugin.manifest, 258 loadedPaths, 259 ) 260 pluginAgents.push(...agents) 261 262 if (agents.length > 0) { 263 logForDebugging( 264 `Loaded ${agents.length} agents from plugin ${plugin.name} default directory`, 265 ) 266 } 267 } catch (error) { 268 logForDebugging( 269 `Failed to load agents from plugin ${plugin.name} default directory: ${error}`, 270 { level: 'error' }, 271 ) 272 } 273 } 274 275 // Load agents from additional paths specified in manifest 276 if (plugin.agentsPaths) { 277 // Process all agentsPaths in parallel. isDuplicatePath is synchronous 278 // (check-and-add), so concurrent access to loadedPaths is safe. 279 const pathResults = await Promise.all( 280 plugin.agentsPaths.map( 281 async (agentPath): Promise<AgentDefinition[]> => { 282 try { 283 const fs = getFsImplementation() 284 const stats = await fs.stat(agentPath) 285 286 if (stats.isDirectory()) { 287 // Load all .md files from directory 288 const agents = await loadAgentsFromDirectory( 289 agentPath, 290 plugin.name, 291 plugin.source, 292 plugin.path, 293 plugin.manifest, 294 loadedPaths, 295 ) 296 297 if (agents.length > 0) { 298 logForDebugging( 299 `Loaded ${agents.length} agents from plugin ${plugin.name} custom path: ${agentPath}`, 300 ) 301 } 302 return agents 303 } else if (stats.isFile() && agentPath.endsWith('.md')) { 304 // Load single agent file 305 const agent = await loadAgentFromFile( 306 agentPath, 307 plugin.name, 308 [], 309 plugin.source, 310 plugin.path, 311 plugin.manifest, 312 loadedPaths, 313 ) 314 if (agent) { 315 logForDebugging( 316 `Loaded agent from plugin ${plugin.name} custom file: ${agentPath}`, 317 ) 318 return [agent] 319 } 320 } 321 return [] 322 } catch (error) { 323 logForDebugging( 324 `Failed to load agents from plugin ${plugin.name} custom path ${agentPath}: ${error}`, 325 { level: 'error' }, 326 ) 327 return [] 328 } 329 }, 330 ), 331 ) 332 for (const agents of pathResults) { 333 pluginAgents.push(...agents) 334 } 335 } 336 return pluginAgents 337 }), 338 ) 339 340 const allAgents = perPluginAgents.flat() 341 logForDebugging(`Total plugin agents loaded: ${allAgents.length}`) 342 return allAgents 343 }, 344) 345 346export function clearPluginAgentCache(): void { 347 loadPluginAgents.cache?.clear?.() 348}