source dump of claude code
at main 272 lines 7.5 kB view raw
1import { mkdir, open, unlink } from 'fs/promises' 2import { join } from 'path' 3import type { SettingSource } from 'src/utils/settings/constants.js' 4import { getManagedFilePath } from 'src/utils/settings/managedPath.js' 5import type { AgentMemoryScope } from '../../tools/AgentTool/agentMemory.js' 6import { 7 type AgentDefinition, 8 isBuiltInAgent, 9 isPluginAgent, 10} from '../../tools/AgentTool/loadAgentsDir.js' 11import { getCwd } from '../../utils/cwd.js' 12import type { EffortValue } from '../../utils/effort.js' 13import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' 14import { getErrnoCode } from '../../utils/errors.js' 15import { AGENT_PATHS } from './types.js' 16 17/** 18 * Formats agent data as markdown file content 19 */ 20export function formatAgentAsMarkdown( 21 agentType: string, 22 whenToUse: string, 23 tools: string[] | undefined, 24 systemPrompt: string, 25 color?: string, 26 model?: string, 27 memory?: AgentMemoryScope, 28 effort?: EffortValue, 29): string { 30 // For YAML double-quoted strings, we need to escape: 31 // - Backslashes: \ -> \\ 32 // - Double quotes: " -> \" 33 // - Newlines: \n -> \\n (so yaml reads it as literal backslash-n, not newline) 34 const escapedWhenToUse = whenToUse 35 .replace(/\\/g, '\\\\') // Escape backslashes first 36 .replace(/"/g, '\\"') // Escape double quotes 37 .replace(/\n/g, '\\\\n') // Escape newlines as \\n so yaml preserves them as \n 38 39 // Omit tools field entirely when tools is undefined or ['*'] (all tools allowed) 40 const isAllTools = 41 tools === undefined || (tools.length === 1 && tools[0] === '*') 42 const toolsLine = isAllTools ? '' : `\ntools: ${tools.join(', ')}` 43 const modelLine = model ? `\nmodel: ${model}` : '' 44 const effortLine = effort !== undefined ? `\neffort: ${effort}` : '' 45 const colorLine = color ? `\ncolor: ${color}` : '' 46 const memoryLine = memory ? `\nmemory: ${memory}` : '' 47 48 return `--- 49name: ${agentType} 50description: "${escapedWhenToUse}"${toolsLine}${modelLine}${effortLine}${colorLine}${memoryLine} 51--- 52 53${systemPrompt} 54` 55} 56 57/** 58 * Gets the directory path for an agent location 59 */ 60function getAgentDirectoryPath(location: SettingSource): string { 61 switch (location) { 62 case 'flagSettings': 63 throw new Error(`Cannot get directory path for ${location} agents`) 64 case 'userSettings': 65 return join(getClaudeConfigHomeDir(), AGENT_PATHS.AGENTS_DIR) 66 case 'projectSettings': 67 return join(getCwd(), AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR) 68 case 'policySettings': 69 return join( 70 getManagedFilePath(), 71 AGENT_PATHS.FOLDER_NAME, 72 AGENT_PATHS.AGENTS_DIR, 73 ) 74 case 'localSettings': 75 return join(getCwd(), AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR) 76 } 77} 78 79function getRelativeAgentDirectoryPath(location: SettingSource): string { 80 switch (location) { 81 case 'projectSettings': 82 return join('.', AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR) 83 default: 84 return getAgentDirectoryPath(location) 85 } 86} 87 88/** 89 * Gets the file path for a new agent based on its name 90 * Used when creating new agent files 91 */ 92export function getNewAgentFilePath(agent: { 93 source: SettingSource 94 agentType: string 95}): string { 96 const dirPath = getAgentDirectoryPath(agent.source) 97 return join(dirPath, `${agent.agentType}.md`) 98} 99 100/** 101 * Gets the actual file path for an agent (handles filename vs agentType mismatch) 102 * Always use this for existing agents to get their real file location 103 */ 104export function getActualAgentFilePath(agent: AgentDefinition): string { 105 if (agent.source === 'built-in') { 106 return 'Built-in' 107 } 108 if (agent.source === 'plugin') { 109 throw new Error('Cannot get file path for plugin agents') 110 } 111 112 const dirPath = getAgentDirectoryPath(agent.source) 113 const filename = agent.filename || agent.agentType 114 return join(dirPath, `${filename}.md`) 115} 116 117/** 118 * Gets the relative file path for a new agent based on its name 119 * Used for displaying where new agent files will be created 120 */ 121export function getNewRelativeAgentFilePath(agent: { 122 source: SettingSource | 'built-in' 123 agentType: string 124}): string { 125 if (agent.source === 'built-in') { 126 return 'Built-in' 127 } 128 const dirPath = getRelativeAgentDirectoryPath(agent.source) 129 return join(dirPath, `${agent.agentType}.md`) 130} 131 132/** 133 * Gets the actual relative file path for an agent (handles filename vs agentType mismatch) 134 */ 135export function getActualRelativeAgentFilePath(agent: AgentDefinition): string { 136 if (isBuiltInAgent(agent)) { 137 return 'Built-in' 138 } 139 if (isPluginAgent(agent)) { 140 return `Plugin: ${agent.plugin || 'Unknown'}` 141 } 142 if (agent.source === 'flagSettings') { 143 return 'CLI argument' 144 } 145 146 const dirPath = getRelativeAgentDirectoryPath(agent.source) 147 const filename = agent.filename || agent.agentType 148 return join(dirPath, `${filename}.md`) 149} 150 151/** 152 * Ensures the directory for an agent location exists 153 */ 154async function ensureAgentDirectoryExists( 155 source: SettingSource, 156): Promise<string> { 157 const dirPath = getAgentDirectoryPath(source) 158 await mkdir(dirPath, { recursive: true }) 159 return dirPath 160} 161 162/** 163 * Saves an agent to the filesystem 164 * @param checkExists - If true, throws error if file already exists 165 */ 166export async function saveAgentToFile( 167 source: SettingSource | 'built-in', 168 agentType: string, 169 whenToUse: string, 170 tools: string[] | undefined, 171 systemPrompt: string, 172 checkExists = true, 173 color?: string, 174 model?: string, 175 memory?: AgentMemoryScope, 176 effort?: EffortValue, 177): Promise<void> { 178 if (source === 'built-in') { 179 throw new Error('Cannot save built-in agents') 180 } 181 182 await ensureAgentDirectoryExists(source) 183 const filePath = getNewAgentFilePath({ source, agentType }) 184 185 const content = formatAgentAsMarkdown( 186 agentType, 187 whenToUse, 188 tools, 189 systemPrompt, 190 color, 191 model, 192 memory, 193 effort, 194 ) 195 try { 196 await writeFileAndFlush(filePath, content, checkExists ? 'wx' : 'w') 197 } catch (e: unknown) { 198 if (getErrnoCode(e) === 'EEXIST') { 199 throw new Error(`Agent file already exists: ${filePath}`) 200 } 201 throw e 202 } 203} 204 205/** 206 * Updates an existing agent file 207 */ 208export async function updateAgentFile( 209 agent: AgentDefinition, 210 newWhenToUse: string, 211 newTools: string[] | undefined, 212 newSystemPrompt: string, 213 newColor?: string, 214 newModel?: string, 215 newMemory?: AgentMemoryScope, 216 newEffort?: EffortValue, 217): Promise<void> { 218 if (agent.source === 'built-in') { 219 throw new Error('Cannot update built-in agents') 220 } 221 222 const filePath = getActualAgentFilePath(agent) 223 224 const content = formatAgentAsMarkdown( 225 agent.agentType, 226 newWhenToUse, 227 newTools, 228 newSystemPrompt, 229 newColor, 230 newModel, 231 newMemory, 232 newEffort, 233 ) 234 235 await writeFileAndFlush(filePath, content) 236} 237 238/** 239 * Deletes an agent file 240 */ 241export async function deleteAgentFromFile( 242 agent: AgentDefinition, 243): Promise<void> { 244 if (agent.source === 'built-in') { 245 throw new Error('Cannot delete built-in agents') 246 } 247 248 const filePath = getActualAgentFilePath(agent) 249 250 try { 251 await unlink(filePath) 252 } catch (e: unknown) { 253 const code = getErrnoCode(e) 254 if (code !== 'ENOENT') { 255 throw e 256 } 257 } 258} 259 260async function writeFileAndFlush( 261 filePath: string, 262 content: string, 263 flag: 'w' | 'wx' = 'w', 264): Promise<void> { 265 const handle = await open(filePath, flag) 266 try { 267 await handle.writeFile(content, { encoding: 'utf-8' }) 268 await handle.datasync() 269 } finally { 270 await handle.close() 271 } 272}