Download src/tools/FileWriteTool/FileWriteTool.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/tools/FileWriteTool/FileWriteTool.ts
- Command line
-
hf download hf://chenbhao/codev/src/tools/FileWriteTool/FileWriteTool.ts
-
curl -L -o FileWriteTool.ts https://huggingface.co/chenbhao/codev/resolve/main/src/tools/FileWriteTool/FileWriteTool.ts
14.9 kB
| 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 = InputSchema | |
| export 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 secrets | |
| const secretError = checkTeamMemSecrets(fullFilePath, content) | |
| if (secretError) { | |
| return { result: false, message: secretError, errorCode: 0 } | |
| } | |
| // Check if path should be ignored based on permission settings | |
| const 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: number | |
| try { | |
| 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 display | |
| for (const dir of newSkillDirs) { | |
| dynamicSkillDirTriggers?.add(dir) | |
| } | |
| // Don't await - let skill loading happen in the background | |
| addSkillDirectories(newSkillDirs).catch(() => {}) | |
| } | |
| // Activate conditional skills whose path patterns match this file | |
| activateConditionalSkillsForPaths([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> | null | |
| try { | |
| 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 shown | |
| clearDeliveredDiagnosticsForFile(`file://${fullFilePath}`) | |
| // didChange: Content has been modified | |
| lspManager.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 view | |
| notifyVscodeFileUpdated(fullFilePath, oldContent, content) | |
| // Update read timestamp, to invalidate stale writes | |
| readFileState.set(fullFilePath, { | |
| content, | |
| timestamp: getFileModificationTime(fullFilePath), | |
| offset: undefined, | |
| limit: undefined, | |
| }) | |
| // Log when writing to CLAUDE.md | |
| if (fullFilePath.endsWith(`${sep}CLAUDE.md`)) { | |
| logEvent('tengu_write_claudemd', {}) | |
| } | |
| let gitDiff: ToolUseDiff | undefined | |
| if ( | |
| 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 = diff | |
| logEvent('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 result | |
| countLinesChanged(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 result | |
| countLinesChanged([], 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>) | |