claude-code-best/src/components/agents/agentFileUtils.ts
claude-code-best 2fb1c9dcd8
feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

273 lines
7.4 KiB
TypeScript

import { mkdir, open, unlink } from 'fs/promises'
import { join } from 'path'
import type { SettingSource } from 'src/utils/settings/constants.js'
import { getManagedFilePath } from 'src/utils/settings/managedPath.js'
import type { AgentMemoryScope } from '@claude-code-best/builtin-tools/tools/AgentTool/agentMemory.js'
import {
type AgentDefinition,
isBuiltInAgent,
isPluginAgent,
} from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { getCwd } from '../../utils/cwd.js'
import type { EffortValue } from '../../utils/effort.js'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
import { getErrnoCode } from '../../utils/errors.js'
import { AGENT_PATHS } from './types.js'
/**
* Formats agent data as markdown file content
*/
export function formatAgentAsMarkdown(
agentType: string,
whenToUse: string,
tools: string[] | undefined,
systemPrompt: string,
color?: string,
model?: string,
memory?: AgentMemoryScope,
effort?: EffortValue,
): string {
// For YAML double-quoted strings, we need to escape:
// - Backslashes: \ -> \\
// - Double quotes: " -> \"
// - Newlines: \n -> \\n (so yaml reads it as literal backslash-n, not newline)
const escapedWhenToUse = whenToUse
.replace(/\\/g, '\\\\') // Escape backslashes first
.replace(/"/g, '\\"') // Escape double quotes
.replace(/\n/g, '\\\\n') // Escape newlines as \\n so yaml preserves them as \n
// Omit tools field entirely when tools is undefined or ['*'] (all tools allowed)
const isAllTools =
tools === undefined || (tools.length === 1 && tools[0] === '*')
const toolsLine = isAllTools ? '' : `\ntools: ${tools.join(', ')}`
const modelLine = model ? `\nmodel: ${model}` : ''
const effortLine = effort !== undefined ? `\neffort: ${effort}` : ''
const colorLine = color ? `\ncolor: ${color}` : ''
const memoryLine = memory ? `\nmemory: ${memory}` : ''
return `---
name: ${agentType}
description: "${escapedWhenToUse}"${toolsLine}${modelLine}${effortLine}${colorLine}${memoryLine}
---
${systemPrompt}
`
}
/**
* Gets the directory path for an agent location
*/
function getAgentDirectoryPath(location: SettingSource): string {
switch (location) {
case 'flagSettings':
throw new Error(`Cannot get directory path for ${location} agents`)
case 'userSettings':
return join(getClaudeConfigHomeDir(), AGENT_PATHS.AGENTS_DIR)
case 'projectSettings':
return join(getCwd(), AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR)
case 'policySettings':
return join(
getManagedFilePath(),
AGENT_PATHS.FOLDER_NAME,
AGENT_PATHS.AGENTS_DIR,
)
case 'localSettings':
return join(getCwd(), AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR)
}
}
function getRelativeAgentDirectoryPath(location: SettingSource): string {
switch (location) {
case 'projectSettings':
return join('.', AGENT_PATHS.FOLDER_NAME, AGENT_PATHS.AGENTS_DIR)
default:
return getAgentDirectoryPath(location)
}
}
/**
* Gets the file path for a new agent based on its name
* Used when creating new agent files
*/
export function getNewAgentFilePath(agent: {
source: SettingSource
agentType: string
}): string {
const dirPath = getAgentDirectoryPath(agent.source)
return join(dirPath, `${agent.agentType}.md`)
}
/**
* Gets the actual file path for an agent (handles filename vs agentType mismatch)
* Always use this for existing agents to get their real file location
*/
export function getActualAgentFilePath(agent: AgentDefinition): string {
if (agent.source === 'built-in') {
return 'Built-in'
}
if (agent.source === 'plugin') {
throw new Error('Cannot get file path for plugin agents')
}
const dirPath = getAgentDirectoryPath(agent.source)
const filename = agent.filename || agent.agentType
return join(dirPath, `${filename}.md`)
}
/**
* Gets the relative file path for a new agent based on its name
* Used for displaying where new agent files will be created
*/
export function getNewRelativeAgentFilePath(agent: {
source: SettingSource | 'built-in'
agentType: string
}): string {
if (agent.source === 'built-in') {
return 'Built-in'
}
const dirPath = getRelativeAgentDirectoryPath(agent.source)
return join(dirPath, `${agent.agentType}.md`)
}
/**
* Gets the actual relative file path for an agent (handles filename vs agentType mismatch)
*/
export function getActualRelativeAgentFilePath(agent: AgentDefinition): string {
if (isBuiltInAgent(agent)) {
return 'Built-in'
}
if (isPluginAgent(agent)) {
return `Plugin: ${agent.plugin || 'Unknown'}`
}
if (agent.source === 'flagSettings') {
return 'CLI argument'
}
const dirPath = getRelativeAgentDirectoryPath(agent.source)
const filename = agent.filename || agent.agentType
return join(dirPath, `${filename}.md`)
}
/**
* Ensures the directory for an agent location exists
*/
async function ensureAgentDirectoryExists(
source: SettingSource,
): Promise<string> {
const dirPath = getAgentDirectoryPath(source)
await mkdir(dirPath, { recursive: true })
return dirPath
}
/**
* Saves an agent to the filesystem
* @param checkExists - If true, throws error if file already exists
*/
export async function saveAgentToFile(
source: SettingSource | 'built-in',
agentType: string,
whenToUse: string,
tools: string[] | undefined,
systemPrompt: string,
checkExists = true,
color?: string,
model?: string,
memory?: AgentMemoryScope,
effort?: EffortValue,
): Promise<void> {
if (source === 'built-in') {
throw new Error('Cannot save built-in agents')
}
await ensureAgentDirectoryExists(source)
const filePath = getNewAgentFilePath({ source, agentType })
const content = formatAgentAsMarkdown(
agentType,
whenToUse,
tools,
systemPrompt,
color,
model,
memory,
effort,
)
try {
await writeFileAndFlush(filePath, content, checkExists ? 'wx' : 'w')
} catch (e: unknown) {
if (getErrnoCode(e) === 'EEXIST') {
throw new Error(`Agent file already exists: ${filePath}`)
}
throw e
}
}
/**
* Updates an existing agent file
*/
export async function updateAgentFile(
agent: AgentDefinition,
newWhenToUse: string,
newTools: string[] | undefined,
newSystemPrompt: string,
newColor?: string,
newModel?: string,
newMemory?: AgentMemoryScope,
newEffort?: EffortValue,
): Promise<void> {
if (agent.source === 'built-in') {
throw new Error('Cannot update built-in agents')
}
const filePath = getActualAgentFilePath(agent)
const content = formatAgentAsMarkdown(
agent.agentType,
newWhenToUse,
newTools,
newSystemPrompt,
newColor,
newModel,
newMemory,
newEffort,
)
await writeFileAndFlush(filePath, content)
}
/**
* Deletes an agent file
*/
export async function deleteAgentFromFile(
agent: AgentDefinition,
): Promise<void> {
if (agent.source === 'built-in') {
throw new Error('Cannot delete built-in agents')
}
const filePath = getActualAgentFilePath(agent)
try {
await unlink(filePath)
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code !== 'ENOENT') {
throw e
}
}
}
async function writeFileAndFlush(
filePath: string,
content: string,
flag: 'w' | 'wx' = 'w',
): Promise<void> {
const handle = await open(filePath, flag)
try {
await handle.writeFile(content, { encoding: 'utf-8' })
await handle.datasync()
} finally {
await handle.close()
}
}