import { dirname, sep } from 'path'import { logEvent } from 'src/services/analytics/index.js'import { z } from 'zod/v4'import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'import { diagnosticTracker } from '../../services/diagnosticTracking.js'import { clearDeliveredDiagnosticsForFile } from '../../services/lsp/LSPDiagnosticRegistry.js'import { getLspServerManager } from '../../services/lsp/manager.js'import { notifyVscodeFileUpdated } from '../../services/mcp/vscodeSdkMcp.js'import { checkTeamMemSecrets } from '../../services/teamMemorySync/teamMemSecretGuard.js'import {activateConditionalSkillsForPaths,addSkillDirectories,discoverSkillDirsForPaths,} from '../../skills/loadSkillsDir.js'import type { ToolUseContext } from '../../Tool.js'import { buildTool, type ToolDef } from '../../Tool.js'import { getCwd } from '../../utils/cwd.js'import { logForDebugging } from '../../utils/debug.js'import { countLinesChanged, getPatchForDisplay } from '../../utils/diff.js'import { isEnvTruthy } from '../../utils/envUtils.js'import { isENOENT } from '../../utils/errors.js'import { getFileModificationTime, writeTextContent } from '../../utils/file.js'import {fileHistoryEnabled,fileHistoryTrackEdit,} from '../../utils/fileHistory.js'import { logFileOperation } from '../../utils/fileOperationAnalytics.js'import { readFileSyncWithMetadata } from '../../utils/fileRead.js'import { getFsImplementation } from '../../utils/fsOperations.js'import {fetchSingleFileGitDiff,type ToolUseDiff,} from '../../utils/gitDiff.js'import { lazySchema } from '../../utils/lazySchema.js'import { logError } from '../../utils/log.js'import { expandPath } from '../../utils/path.js'import {checkWritePermissionForTool,matchingRuleForInput,} from '../../utils/permissions/filesystem.js'import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'import { matchWildcardPattern } from '../../utils/permissions/shellRuleMatching.js'import { FILE_UNEXPECTEDLY_MODIFIED_ERROR } from '../FileEditTool/constants.js'import { gitDiffSchema, hunkSchema } from '../FileEditTool/types.js'import { FILE_WRITE_TOOL_NAME, getWriteToolDescription } from './prompt.js'import {getToolUseSummary,isResultTruncated,renderToolResultMessage,renderToolUseErrorMessage,renderToolUseMessage,renderToolUseRejectedMessage,userFacingName,} from './UI.js'const inputSchema = lazySchema(() =>z.strictObject({file_path: z.string().describe('The absolute path to the file to write (must be absolute, not relative)',),content: z.string().describe('The content to write to the file'),}),)type InputSchema = ReturnType<typeof inputSchema>const outputSchema = lazySchema(() =>z.object({type: z.enum(['create', 'update']).describe('Whether a new file was created or an existing file was updated',),filePath: z.string().describe('The path to the file that was written'),content: z.string().describe('The content that was written to the file'),structuredPatch: z.array(hunkSchema()).describe('Diff patch showing the changes'),originalFile: z.string().nullable().describe('The original file content before the write (null for new files)',),gitDiff: gitDiffSchema().optional(),}),)type OutputSchema = ReturnType<typeof outputSchema>export type Output = z.infer<OutputSchema>export type FileWriteToolInput = InputSchemaexport const FileWriteTool = buildTool({name: FILE_WRITE_TOOL_NAME,searchHint: 'create or overwrite files',maxResultSizeChars: 100_000,strict: true,async description() {return 'Write a file to the local filesystem.'},userFacingName,getToolUseSummary,getActivityDescription(input) {const summary = getToolUseSummary(input)return summary ? `Writing ${summary}` : 'Writing file'},async prompt() {return getWriteToolDescription()},renderToolUseMessage,isResultTruncated,get inputSchema(): InputSchema {return inputSchema()},get outputSchema(): OutputSchema {return outputSchema()},toAutoClassifierInput(input) {return `${input.file_path}: ${input.content}`},getPath(input): string {return input.file_path},backfillObservableInput(input) {// hooks.mdx documents file_path as absolute; expand so hook allowlists// can't be bypassed via ~ or relative paths.if (typeof input.file_path === 'string') {input.file_path = expandPath(input.file_path)}},async preparePermissionMatcher({ file_path }) {return pattern => matchWildcardPattern(pattern, file_path)},async checkPermissions(input, context): Promise<PermissionDecision> {const appState = context.getAppState()return checkWritePermissionForTool(FileWriteTool,input,appState.toolPermissionContext,)},renderToolUseRejectedMessage,renderToolUseErrorMessage,renderToolResultMessage,extractSearchText() {// Transcript render shows either content (create, via HighlightedCode)// or a structured diff (update). The heuristic's 'content' allowlist key// would index the raw content string even in update mode where it's NOT// shown — phantom. Under-count: tool_use already indexes file_path.return ''},async validateInput({ file_path, content }, toolUseContext: ToolUseContext) {const fullFilePath = expandPath(file_path)// Reject writes to team memory files that contain secretsconst secretError = checkTeamMemSecrets(fullFilePath, content)if (secretError) {return { result: false, message: secretError, errorCode: 0 }}// Check if path should be ignored based on permission settingsconst appState = toolUseContext.getAppState()const denyRule = matchingRuleForInput(fullFilePath,appState.toolPermissionContext,'edit','deny',)if (denyRule !== null) {return {result: false,message:'File is in a directory that is denied by your permission settings.',errorCode: 1,}}// SECURITY: Skip filesystem operations for UNC paths to prevent NTLM credential leaks.// On Windows, fs.existsSync() on UNC paths triggers SMB authentication which could// leak credentials to malicious servers. Let the permission check handle UNC paths.if (fullFilePath.startsWith('\\\\') || fullFilePath.startsWith('//')) {return { result: true }}const fs = getFsImplementation()let fileMtimeMs: numbertry {const fileStat = await fs.stat(fullFilePath)fileMtimeMs = fileStat.mtimeMs} catch (e) {if (isENOENT(e)) {return { result: true }}throw e}const readTimestamp = toolUseContext.readFileState.get(fullFilePath)if (!readTimestamp || readTimestamp.isPartialView) {return {result: false,message:'File has not been read yet. Read it first before writing to it.',errorCode: 2,}}// Reuse mtime from the stat above — avoids a redundant statSync via// getFileModificationTime. The readTimestamp guard above ensures this// block is always reached when the file exists.const lastWriteTime = Math.floor(fileMtimeMs)if (lastWriteTime > readTimestamp.timestamp) {return {result: false,message:'File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.',errorCode: 3,}}return { result: true }},async call({ file_path, content },{ readFileState, updateFileHistoryState, dynamicSkillDirTriggers },_,parentMessage,) {const fullFilePath = expandPath(file_path)const dir = dirname(fullFilePath)// Discover skills from this file's path (fire-and-forget, non-blocking)const cwd = getCwd()const newSkillDirs = await discoverSkillDirsForPaths([fullFilePath], cwd)if (newSkillDirs.length > 0) {// Store discovered dirs for attachment displayfor (const dir of newSkillDirs) {dynamicSkillDirTriggers?.add(dir)}// Don't await - let skill loading happen in the backgroundaddSkillDirectories(newSkillDirs).catch(() => {})}// Activate conditional skills whose path patterns match this fileactivateConditionalSkillsForPaths([fullFilePath], cwd)await diagnosticTracker.beforeFileEdited(fullFilePath)// Ensure parent directory exists before the atomic read-modify-write section.// Must stay OUTSIDE the critical section below (a yield between the staleness// check and writeTextContent lets concurrent edits interleave), and BEFORE the// write (lazy-mkdir-on-ENOENT would fire a spurious tengu_atomic_write_error// inside writeFileSyncAndFlush_DEPRECATED before ENOENT propagates back).await getFsImplementation().mkdir(dir)if (fileHistoryEnabled()) {// Backup captures pre-edit content — safe to call before the staleness// check (idempotent v1 backup keyed on content hash; if staleness fails// later we just have an unused backup, not corrupt state).await fileHistoryTrackEdit(updateFileHistoryState,fullFilePath,parentMessage.uuid,)}// Load current state and confirm no changes since last read.// Please avoid async operations between here and writing to disk to preserve atomicity.let meta: ReturnType<typeof readFileSyncWithMetadata> | nulltry {meta = readFileSyncWithMetadata(fullFilePath)} catch (e) {if (isENOENT(e)) {meta = null} else {throw e}}if (meta !== null) {const lastWriteTime = getFileModificationTime(fullFilePath)const lastRead = readFileState.get(fullFilePath)if (!lastRead || lastWriteTime > lastRead.timestamp) {// Timestamp indicates modification, but on Windows timestamps can change// without content changes (cloud sync, antivirus, etc.). For full reads,// compare content as a fallback to avoid false positives.const isFullRead =lastRead &&lastRead.offset === undefined &&lastRead.limit === undefined// meta.content is CRLF-normalized — matches readFileState's normalized form.if (!isFullRead || meta.content !== lastRead.content) {throw new Error(FILE_UNEXPECTEDLY_MODIFIED_ERROR)}}}const enc = meta?.encoding ?? 'utf8'const oldContent = meta?.content ?? null// Write is a full content replacement — the model sent explicit line endings// in `content` and meant them. Do not rewrite them. Previously we preserved// the old file's line endings (or sampled the repo via ripgrep for new// files), which silently corrupted e.g. bash scripts with \r on Linux when// overwriting a CRLF file or when binaries in cwd poisoned the repo sample.writeTextContent(fullFilePath, content, enc, 'LF')// Notify LSP servers about file modification (didChange) and save (didSave)const lspManager = getLspServerManager()if (lspManager) {// Clear previously delivered diagnostics so new ones will be shownclearDeliveredDiagnosticsForFile(`file://${fullFilePath}`)// didChange: Content has been modifiedlspManager.changeFile(fullFilePath, content).catch((err: Error) => {logForDebugging(`LSP: Failed to notify server of file change for ${fullFilePath}: ${err.message}`,)logError(err)})// didSave: File has been saved to disk (triggers diagnostics in TypeScript server)lspManager.saveFile(fullFilePath).catch((err: Error) => {logForDebugging(`LSP: Failed to notify server of file save for ${fullFilePath}: ${err.message}`,)logError(err)})}// Notify VSCode about the file change for diff viewnotifyVscodeFileUpdated(fullFilePath, oldContent, content)// Update read timestamp, to invalidate stale writesreadFileState.set(fullFilePath, {content,timestamp: getFileModificationTime(fullFilePath),offset: undefined,limit: undefined,})// Log when writing to CLAUDE.mdif (fullFilePath.endsWith(`${sep}CLAUDE.md`)) {logEvent('tengu_write_claudemd', {})}let gitDiff: ToolUseDiff | undefinedif (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&getFeatureValue_CACHED_MAY_BE_STALE('tengu_quartz_lantern', false)) {const startTime = Date.now()const diff = await fetchSingleFileGitDiff(fullFilePath)if (diff) gitDiff = difflogEvent('tengu_tool_use_diff_computed', {isWriteTool: true,durationMs: Date.now() - startTime,hasDiff: !!diff,})}if (oldContent) {const patch = getPatchForDisplay({filePath: file_path,fileContents: oldContent,edits: [{old_string: oldContent,new_string: content,replace_all: false,},],})const data = {type: 'update' as const,filePath: file_path,content,structuredPatch: patch,originalFile: oldContent,...(gitDiff && { gitDiff }),}// Track lines added and removed for file updates, right before yielding resultcountLinesChanged(patch)logFileOperation({operation: 'write',tool: 'FileWriteTool',filePath: fullFilePath,type: 'update',})return {data,}}const data = {type: 'create' as const,filePath: file_path,content,structuredPatch: [],originalFile: null,...(gitDiff && { gitDiff }),}// For creation of new files, count all lines as additions, right before yielding the resultcountLinesChanged([], content)logFileOperation({operation: 'write',tool: 'FileWriteTool',filePath: fullFilePath,type: 'create',})return {data,}},mapToolResultToToolResultBlockParam({ filePath, type }, toolUseID) {switch (type) {case 'create':return {tool_use_id: toolUseID,type: 'tool_result',content: `File created successfully at: ${filePath}`,}case 'update':return {tool_use_id: toolUseID,type: 'tool_result',content: `The file ${filePath} has been updated successfully.`,}}},} satisfies ToolDef<InputSchema, Output>)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。