/** * KHAELOR * File: src/tools/write.ts * Description: The write tool — complete-file writes with overwrite protection and header reminders (TOOL_PROTOCOL §3). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { isWorkspaceError } from "../workspace/index.js"; import { capDiff, unifiedDiff } from "./diff.js"; import { HEADER_REMINDER_NOTE, countContentLines, displayPath, hasKhaelorHeader, requiresKhaelorHeader, resolveToolPath, } from "./format.js"; import type { ToolDefinition } from "./registry.js"; import type { ToolContext, ToolResult } from "./types.js"; export interface WriteInput { file_path: string; content: string; } const DESCRIPTION = "Write a complete file to the workspace, creating it (and parent directories) if needed, or fully " + "replacing its content if it exists. To modify part of an existing file, use the edit tool instead — " + "write replaces the whole file. You must have read an existing file with the read tool during this " + "session before overwriting it; if the file changed on disk since you read it, the write is refused " + "and you must re-read first. New source files in this project must begin with the mandatory KHAELOR " + "author header (Author: Simon-Pierre Boucher, Contact: contact@spboucher.ai, File, Description)."; function errorResult(content: string, title: string, startedAt: number): ToolResult { return { content, isError: true, metadata: { title, durationMs: Date.now() - startedAt } }; } async function executeWrite(input: WriteInput, ctx: ToolContext): Promise { const startedAt = Date.now(); const cwd = ctx.workspace.cwd(); const abs = resolveToolPath(cwd, input.file_path); const rel = displayPath(cwd, abs); // Detect the existing file state (TOOL_PROTOCOL §3.2 — overwrite protection). let oldContent: string | undefined; try { oldContent = await ctx.workspace.readFile(abs); } catch (cause) { if (isWorkspaceError(cause)) { if (cause.code === "file-is-directory") { return errorResult( `Path is a directory, not a file: ${abs}. Write to a file path inside it instead.`, `Write ${rel} · error`, startedAt, ); } if (cause.code === "file-binary" || cause.code === "file-too-large") { // The file exists but could never have been read this session. return errorResult( `Refusing to overwrite ${abs}: you have not read this file in this session. Read it first so you do not destroy existing content, then write or edit it.`, `Write ${rel} · refused`, startedAt, ); } // file-not-found → a new file. } else { throw cause; } } if (oldContent !== undefined) { if (ctx.fileTimes.get(abs) === undefined) { return errorResult( `Refusing to overwrite ${abs}: you have not read this file in this session. Read it first so you do not destroy existing content, then write or edit it.`, `Write ${rel} · refused`, startedAt, ); } if (ctx.fileTimes.check(abs, oldContent) === "externally-modified") { return errorResult( `Refusing to overwrite ${abs}: the file changed on disk after you last read it (content hash mismatch). Someone else may be editing it. Re-read the file and reapply your change.`, `Write ${rel} · refused`, startedAt, ); } } const created = oldContent === undefined; const diff = unifiedDiff(oldContent ?? "", input.content, `a/${rel}`, `b/${rel}`); try { await ctx.workspace.writeFile(abs, input.content); } catch (cause) { return errorResult( `Failed to write ${abs}: ${cause instanceof Error ? cause.message : String(cause)}. Check the path and try again.`, `Write ${rel} · error`, startedAt, ); } ctx.fileTimes.stamp(abs, input.content); ctx.emit({ type: "file.modified", payload: { path: rel, operation: "write", diffStats: { added: diff.additions, removed: diff.deletions }, diff: capDiff(diff.text), toolUseId: ctx.callId, }, }); const newLines = countContentLines(input.content); let content: string; if (created) { content = `Wrote ${abs} (${newLines} lines).`; } else { const oldLines = countContentLines(oldContent ?? ""); content = `Replaced ${abs} (was ${oldLines} lines, now ${newLines} lines).`; } // Mandatory header reminder (Absolute Rule #0) — new project source files only. if (created && requiresKhaelorHeader(cwd, abs) && !hasKhaelorHeader(input.content)) { content += `\n\n${HEADER_REMINDER_NOTE}`; } return { content, metadata: { title: created ? `Write ${rel} · new file · ${newLines} lines` : `Write ${rel} · +${diff.additions} −${diff.deletions}`, diff: diff.text, additions: diff.additions, deletions: diff.deletions, durationMs: Date.now() - startedAt, extra: { created }, }, }; } /** Create the write tool definition. */ export function createWriteTool(): ToolDefinition { return { name: "write", description: DESCRIPTION, capability: "file.write", inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Path of the file to write (absolute preferred).", }, content: { type: "string", description: "The complete new content of the file.", }, }, required: ["file_path", "content"], }, execute: executeWrite, }; }