mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
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>
This commit is contained in:
198
packages/builtin-tools/src/tools/GlobTool/GlobTool.ts
Normal file
198
packages/builtin-tools/src/tools/GlobTool/GlobTool.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ValidationResult } from 'src/Tool.js'
|
||||
import { buildTool, type ToolDef } from 'src/Tool.js'
|
||||
import { getCwd } from 'src/utils/cwd.js'
|
||||
import { isENOENT } from 'src/utils/errors.js'
|
||||
import {
|
||||
FILE_NOT_FOUND_CWD_NOTE,
|
||||
suggestPathUnderCwd,
|
||||
} from 'src/utils/file.js'
|
||||
import { getFsImplementation } from 'src/utils/fsOperations.js'
|
||||
import { glob } from 'src/utils/glob.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import { expandPath, toRelativePath } from 'src/utils/path.js'
|
||||
import { checkReadPermissionForTool } from 'src/utils/permissions/filesystem.js'
|
||||
import type { PermissionDecision } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { matchWildcardPattern } from 'src/utils/permissions/shellRuleMatching.js'
|
||||
import { DESCRIPTION, GLOB_TOOL_NAME } from './prompt.js'
|
||||
import {
|
||||
getToolUseSummary,
|
||||
renderToolResultMessage,
|
||||
renderToolUseErrorMessage,
|
||||
renderToolUseMessage,
|
||||
userFacingName,
|
||||
} from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
pattern: z.string().describe('The glob pattern to match files against'),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
durationMs: z
|
||||
.number()
|
||||
.describe('Time taken to execute the search in milliseconds'),
|
||||
numFiles: z.number().describe('Total number of files found'),
|
||||
filenames: z
|
||||
.array(z.string())
|
||||
.describe('Array of file paths that match the pattern'),
|
||||
truncated: z
|
||||
.boolean()
|
||||
.describe('Whether results were truncated (limited to 100 files)'),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
export const GlobTool = buildTool({
|
||||
name: GLOB_TOOL_NAME,
|
||||
searchHint: 'find files by name pattern or wildcard',
|
||||
maxResultSizeChars: 100_000,
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
userFacingName,
|
||||
getToolUseSummary,
|
||||
getActivityDescription(input) {
|
||||
const summary = getToolUseSummary(input)
|
||||
return summary ? `Finding ${summary}` : 'Finding files'
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
toAutoClassifierInput(input) {
|
||||
return input.pattern
|
||||
},
|
||||
isSearchOrReadCommand() {
|
||||
return { isSearch: true, isRead: false }
|
||||
},
|
||||
getPath({ path }): string {
|
||||
return path ? expandPath(path) : getCwd()
|
||||
},
|
||||
async preparePermissionMatcher({ pattern }) {
|
||||
return rulePattern => matchWildcardPattern(rulePattern, pattern)
|
||||
},
|
||||
async validateInput({ path }): Promise<ValidationResult> {
|
||||
// If path is provided, validate that it exists and is a directory
|
||||
if (path) {
|
||||
const fs = getFsImplementation()
|
||||
const absolutePath = expandPath(path)
|
||||
|
||||
// SECURITY: Skip filesystem operations for UNC paths to prevent NTLM credential leaks.
|
||||
if (absolutePath.startsWith('\\\\') || absolutePath.startsWith('//')) {
|
||||
return { result: true }
|
||||
}
|
||||
|
||||
let stats
|
||||
try {
|
||||
stats = await fs.stat(absolutePath)
|
||||
} catch (e: unknown) {
|
||||
if (isENOENT(e)) {
|
||||
const cwdSuggestion = await suggestPathUnderCwd(absolutePath)
|
||||
let message = `Directory does not exist: ${path}. ${FILE_NOT_FOUND_CWD_NOTE} ${getCwd()}.`
|
||||
if (cwdSuggestion) {
|
||||
message += ` Did you mean ${cwdSuggestion}?`
|
||||
}
|
||||
return {
|
||||
result: false,
|
||||
message,
|
||||
errorCode: 1,
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return {
|
||||
result: false,
|
||||
message: `Path is not a directory: ${path}`,
|
||||
errorCode: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { result: true }
|
||||
},
|
||||
async checkPermissions(input, context): Promise<PermissionDecision> {
|
||||
const appState = context.getAppState()
|
||||
return checkReadPermissionForTool(
|
||||
GlobTool,
|
||||
input,
|
||||
appState.toolPermissionContext,
|
||||
)
|
||||
},
|
||||
async prompt() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
renderToolUseMessage,
|
||||
renderToolUseErrorMessage,
|
||||
renderToolResultMessage,
|
||||
// Reuses Grep's render (UI.tsx:65) — shows filenames.join. durationMs/
|
||||
// numFiles are "Found 3 files in 12ms" chrome (under-count, fine).
|
||||
extractSearchText({ filenames }) {
|
||||
return filenames.join('\n')
|
||||
},
|
||||
async call(input, { abortController, getAppState, globLimits }) {
|
||||
const start = Date.now()
|
||||
const appState = getAppState()
|
||||
const limit = globLimits?.maxResults ?? 100
|
||||
const { files, truncated } = await glob(
|
||||
input.pattern,
|
||||
GlobTool.getPath(input),
|
||||
{ limit, offset: 0 },
|
||||
abortController.signal,
|
||||
appState.toolPermissionContext,
|
||||
)
|
||||
// Relativize paths under cwd to save tokens (same as GrepTool)
|
||||
const filenames = files.map(toRelativePath)
|
||||
const output: Output = {
|
||||
filenames,
|
||||
durationMs: Date.now() - start,
|
||||
numFiles: filenames.length,
|
||||
truncated,
|
||||
}
|
||||
return {
|
||||
data: output,
|
||||
}
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
||||
if (output.filenames.length === 0) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: 'No files found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: [
|
||||
...output.filenames,
|
||||
...(output.truncated
|
||||
? [
|
||||
'(Results are truncated. Consider using a more specific path or pattern.)',
|
||||
]
|
||||
: []),
|
||||
].join('\n'),
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
65
packages/builtin-tools/src/tools/GlobTool/UI.tsx
Normal file
65
packages/builtin-tools/src/tools/GlobTool/UI.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React from 'react'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { extractTag } from 'src/utils/messages.js'
|
||||
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js'
|
||||
import { Text } from '@anthropic/ink'
|
||||
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from 'src/utils/file.js'
|
||||
import { truncate } from 'src/utils/format.js'
|
||||
import { GrepTool } from '../GrepTool/GrepTool.js'
|
||||
|
||||
export function userFacingName(): string {
|
||||
return 'Search'
|
||||
}
|
||||
|
||||
export function renderToolUseMessage(
|
||||
{ pattern, path }: Partial<{ pattern: string; path: string }>,
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (!pattern) {
|
||||
return null
|
||||
}
|
||||
if (!path) {
|
||||
return `pattern: "${pattern}"`
|
||||
}
|
||||
return `pattern: "${pattern}", path: "${verbose ? path : getDisplayPath(path)}"`
|
||||
}
|
||||
|
||||
export function renderToolUseErrorMessage(
|
||||
result: ToolResultBlockParam['content'],
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (
|
||||
!verbose &&
|
||||
typeof result === 'string' &&
|
||||
extractTag(result, 'tool_use_error')
|
||||
) {
|
||||
const errorMessage = extractTag(result, 'tool_use_error')
|
||||
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text color="error">File not found</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text color="error">Error searching files</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
|
||||
}
|
||||
|
||||
// Note: GlobTool reuses GrepTool's renderToolResultMessage
|
||||
export const renderToolResultMessage = GrepTool.renderToolResultMessage
|
||||
|
||||
export function getToolUseSummary(
|
||||
input: Partial<{ pattern: string; path: string }> | undefined,
|
||||
): string | null {
|
||||
if (!input?.pattern) {
|
||||
return null
|
||||
}
|
||||
return truncate(input.pattern, TOOL_SUMMARY_MAX_LENGTH)
|
||||
}
|
||||
7
packages/builtin-tools/src/tools/GlobTool/prompt.ts
Normal file
7
packages/builtin-tools/src/tools/GlobTool/prompt.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const GLOB_TOOL_NAME = 'Glob'
|
||||
|
||||
export const DESCRIPTION = `- 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 Agent tool instead`
|
||||
@@ -0,0 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type MessageResponse = any;
|
||||
@@ -0,0 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type extractTag = any;
|
||||
Reference in New Issue
Block a user