diff --git a/packages/core/src/agent/presets/plan.ts b/packages/core/src/agent/presets/plan.ts index 8372f28..5910832 100644 --- a/packages/core/src/agent/presets/plan.ts +++ b/packages/core/src/agent/presets/plan.ts @@ -2,33 +2,63 @@ import type { AgentInfo } from '../types.js'; /** * Plan Agent 专用提示词 + * + * 变量映射: + * - GLOB_TOOL_NAME -> glob + * - GREP_TOOL_NAME -> grep_content + * - READ_TOOL_NAME -> read_file + * - BASH_TOOL_NAME -> bash */ -const PLAN_PROMPT = ` -# Plan Mode - System Reminder +const PLAN_PROMPT = `You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans. -CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN: -ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat, -or ANY other bash command to manipulate files - commands may ONLY read/inspect. -This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user -edit requests. You may ONLY observe, analyze, and plan. Any modification attempt -is a critical violation. ZERO exceptions. +=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS === +This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from: +- Creating new files (no Write, touch, or file creation of any kind) +- Modifying existing files (no Edit operations) +- Deleting files (no rm or deletion) +- Moving or copying files (no mv or cp) +- Creating temporary files anywhere, including /tmp +- Using redirect operators (>, >>, |) or heredocs to write to files +- Running ANY commands that change system state ---- +Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail. -## Responsibility +You will be provided with a set of requirements and optionally a perspective on how to approach the design process. -Your current responsibility is to think, read, search, and delegate explore agents to construct a well formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity. +## Your Process -Ask the user clarifying questions or ask for their opinion when weighing tradeoffs. +1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process. -**NOTE:** At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. +2. **Explore Thoroughly**: + - Read any files provided to you in the initial prompt + - Find existing patterns and conventions using glob, grep_content, and read_file + - Understand the current architecture + - Identify similar features as reference + - Trace through relevant code paths + - Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail) + - NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification ---- +3. **Design Solution**: + - Create implementation approach based on your assigned perspective + - Consider trade-offs and architectural decisions + - Follow existing patterns where appropriate -## Important +4. **Detail the Plan**: + - Provide step-by-step implementation strategy + - Identify dependencies and sequencing + - Anticipate potential challenges -The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. -`; +## Required Output + +End your response with: + +### Critical Files for Implementation +List 3-5 files most critical for implementing this plan: +- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"] +- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"] +- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"] + +REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files. You do NOT have access to file editing tools.`; /** * 计划 Agent @@ -50,6 +80,7 @@ export const planAgent: Omit = { 'write_file', 'list_directory', 'search_files', + 'glob', 'grep_content', 'get_file_info', // Git 只读 diff --git a/packages/core/src/tools/descriptions/filesystem/glob.txt b/packages/core/src/tools/descriptions/filesystem/glob.txt new file mode 100644 index 0000000..fe115b0 --- /dev/null +++ b/packages/core/src/tools/descriptions/filesystem/glob.txt @@ -0,0 +1,6 @@ +- Fast file pattern matching tool that works with any codebase size +- Supports glob patterns like "**/*.js" or "src/**/*.ts" +- Returns matching file paths sorted by modification time +- Use this tool when you need to find files by name patterns +- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead +- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful. diff --git a/packages/core/src/tools/filesystem/glob.ts b/packages/core/src/tools/filesystem/glob.ts new file mode 100644 index 0000000..a43f865 --- /dev/null +++ b/packages/core/src/tools/filesystem/glob.ts @@ -0,0 +1,208 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; +import { loadDescription } from '../load_description.js'; +import { getPermissionManager } from '../../permission/index.js'; + +/** + * 简单的 glob 模式匹配 + * 支持 ** (任意目录) 和 * (任意字符) + */ +function globToRegex(pattern: string): RegExp { + // 转义正则特殊字符,但保留 * 和 ** + let regexStr = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符 + .replace(/\*\*/g, '{{GLOBSTAR}}') // 临时替换 ** + .replace(/\*/g, '[^/]*') // * 匹配非 / 字符 + .replace(/\?/g, '[^/]') // ? 匹配单个非 / 字符 + .replace(/\{\{GLOBSTAR\}\}/g, '.*'); // ** 匹配任意字符包括 / + + return new RegExp(`^${regexStr}$`, 'i'); +} + +/** + * 检查是否应该忽略的目录/文件 + */ +function shouldIgnore(name: string): boolean { + const ignorePatterns = [ + 'node_modules', + '.git', + '.svn', + '.hg', + 'dist', + 'build', + 'coverage', + '.cache', + '.vscode', + '.idea', + '__pycache__', + '.pytest_cache', + '.mypy_cache', + 'venv', + '.venv', + 'target', // Rust + 'vendor', // Go + ]; + return name.startsWith('.') || ignorePatterns.includes(name); +} + +interface FileInfo { + path: string; + mtime: number; +} + +export const globTool: ToolWithMetadata = { + name: 'glob', + description: loadDescription('glob'), + metadata: { + name: 'glob', + category: 'filesystem', + description: '使用 glob 模式匹配文件', + keywords: ['glob', 'pattern', 'match', 'file', 'search', '模式', '匹配', '文件'], + deferLoading: false, // 常用工具,不延迟加载 + }, + parameters: { + pattern: { + type: 'string', + description: '要匹配的 glob 模式(如 "**/*.ts" 或 "src/**/*.js")', + required: true, + }, + path: { + type: 'string', + description: + '搜索的目录。如果不指定,使用当前工作目录。重要:省略此字段使用默认目录,不要输入 "undefined" 或 "null"。', + required: false, + }, + }, + execute: async (params: Record): Promise => { + const pattern = params.pattern as string; + const searchPath = params.path as string | undefined; + const cwd = process.cwd(); + + // 解析搜索目录 + let searchDir: string; + if (searchPath) { + searchDir = path.isAbsolute(searchPath) ? searchPath : path.resolve(cwd, searchPath); + } else { + searchDir = cwd; + } + + // 权限检查 + const permissionManager = getPermissionManager(); + const permResult = await permissionManager.checkFilePermission({ + operation: 'search', + path: searchDir, + workdir: cwd, + }); + + if (!permResult.allowed) { + if (permResult.needsConfirmation) { + return { + success: false, + output: '', + error: `需要用户确认: 搜索目录 ${searchDir}\n原因: ${permResult.reason || '需要权限确认'}`, + }; + } + return { + success: false, + output: '', + error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`, + }; + } + + const limit = 100; + const files: FileInfo[] = []; + let truncated = false; + + // 编译 glob 模式 + const regex = globToRegex(pattern); + + async function searchRecursive(dir: string, relativePath: string = ''): Promise { + if (files.length >= limit) { + truncated = true; + return; + } + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (files.length >= limit) { + truncated = true; + return; + } + + const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // 忽略特定目录 + if (!shouldIgnore(entry.name)) { + await searchRecursive(fullPath, entryRelPath); + } + } else { + // 检查文件是否匹配模式 + if (regex.test(entryRelPath)) { + try { + const stat = await fs.stat(fullPath); + files.push({ + path: fullPath, + mtime: stat.mtimeMs, + }); + } catch { + // 忽略无法访问的文件 + files.push({ + path: fullPath, + mtime: 0, + }); + } + } + } + } + } catch { + // 忽略权限错误等 + } + } + + try { + // 检查搜索目录是否存在 + const dirStat = await fs.stat(searchDir); + if (!dirStat.isDirectory()) { + return { + success: false, + output: '', + error: `路径不是目录: ${searchDir}`, + }; + } + + await searchRecursive(searchDir); + + // 按修改时间排序(最新的在前) + files.sort((a, b) => b.mtime - a.mtime); + + const output: string[] = []; + + if (files.length === 0) { + output.push('No files found'); + } else { + output.push(...files.map((f) => f.path)); + if (truncated) { + output.push(''); + output.push('(Results are truncated. Consider using a more specific path or pattern.)'); + } + } + + return { + success: true, + output: output.join('\n'), + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +}; diff --git a/packages/core/src/tools/filesystem/index.ts b/packages/core/src/tools/filesystem/index.ts index 3f8a5bb..8719448 100644 --- a/packages/core/src/tools/filesystem/index.ts +++ b/packages/core/src/tools/filesystem/index.ts @@ -10,6 +10,7 @@ export { createDirectoryTool } from './create_directory.js'; // 搜索 export { searchFilesTool } from './search_files.js'; +export { globTool } from './glob.js'; export { grepContentTool } from './grep_content.js'; // 文件信息 diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 71340b2..23d6454 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -23,6 +23,7 @@ import { listDirTool, createDirectoryTool, searchFilesTool, + globTool, grepContentTool, getFileInfoTool, moveFileTool, @@ -88,6 +89,7 @@ const allToolsWithMetadata: ToolWithMetadata[] = [ listDirTool, createDirectoryTool, searchFilesTool, + globTool, grepContentTool, getFileInfoTool, moveFileTool, diff --git a/packages/core/src/tools/load_description.ts b/packages/core/src/tools/load_description.ts index c43accf..c1fa5a4 100644 --- a/packages/core/src/tools/load_description.ts +++ b/packages/core/src/tools/load_description.ts @@ -17,6 +17,7 @@ const TOOL_CATEGORY_MAP: Record = { list_directory: 'filesystem', create_directory: 'filesystem', search_files: 'filesystem', + glob: 'filesystem', grep_content: 'filesystem', get_file_info: 'filesystem', move_file: 'filesystem',