Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
15 kB · 434 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435import { 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>)