spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/tools/write.ts4 * Description: The write tool — complete-file writes with overwrite protection and header reminders (TOOL_PROTOCOL §3).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { isWorkspaceError } from "../workspace/index.js";11import { capDiff, unifiedDiff } from "./diff.js";12import {13 HEADER_REMINDER_NOTE,14 countContentLines,15 displayPath,16 hasKhaelorHeader,17 requiresKhaelorHeader,18 resolveToolPath,19} from "./format.js";20import type { ToolDefinition } from "./registry.js";21import type { ToolContext, ToolResult } from "./types.js";2223export interface WriteInput {24 file_path: string;25 content: string;26}2728const DESCRIPTION =29 "Write a complete file to the workspace, creating it (and parent directories) if needed, or fully " +30 "replacing its content if it exists. To modify part of an existing file, use the edit tool instead — " +31 "write replaces the whole file. You must have read an existing file with the read tool during this " +32 "session before overwriting it; if the file changed on disk since you read it, the write is refused " +33 "and you must re-read first. New source files in this project must begin with the mandatory KHAELOR " +34 "author header (Author: Simon-Pierre Boucher, Contact: contact@spboucher.ai, File, Description).";3536function errorResult(content: string, title: string, startedAt: number): ToolResult {37 return { content, isError: true, metadata: { title, durationMs: Date.now() - startedAt } };38}3940async function executeWrite(input: WriteInput, ctx: ToolContext): Promise<ToolResult> {41 const startedAt = Date.now();42 const cwd = ctx.workspace.cwd();43 const abs = resolveToolPath(cwd, input.file_path);44 const rel = displayPath(cwd, abs);4546 // Detect the existing file state (TOOL_PROTOCOL §3.2 — overwrite protection).47 let oldContent: string | undefined;48 try {49 oldContent = await ctx.workspace.readFile(abs);50 } catch (cause) {51 if (isWorkspaceError(cause)) {52 if (cause.code === "file-is-directory") {53 return errorResult(54 `Path is a directory, not a file: ${abs}. Write to a file path inside it instead.`,55 `Write ${rel} · error`,56 startedAt,57 );58 }59 if (cause.code === "file-binary" || cause.code === "file-too-large") {60 // The file exists but could never have been read this session.61 return errorResult(62 `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.`,63 `Write ${rel} · refused`,64 startedAt,65 );66 }67 // file-not-found → a new file.68 } else {69 throw cause;70 }71 }7273 if (oldContent !== undefined) {74 if (ctx.fileTimes.get(abs) === undefined) {75 return errorResult(76 `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.`,77 `Write ${rel} · refused`,78 startedAt,79 );80 }81 if (ctx.fileTimes.check(abs, oldContent) === "externally-modified") {82 return errorResult(83 `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.`,84 `Write ${rel} · refused`,85 startedAt,86 );87 }88 }8990 const created = oldContent === undefined;91 const diff = unifiedDiff(oldContent ?? "", input.content, `a/${rel}`, `b/${rel}`);9293 try {94 await ctx.workspace.writeFile(abs, input.content);95 } catch (cause) {96 return errorResult(97 `Failed to write ${abs}: ${cause instanceof Error ? cause.message : String(cause)}. Check the path and try again.`,98 `Write ${rel} · error`,99 startedAt,100 );101 }102103 ctx.fileTimes.stamp(abs, input.content);104 ctx.emit({105 type: "file.modified",106 payload: {107 path: rel,108 operation: "write",109 diffStats: { added: diff.additions, removed: diff.deletions },110 diff: capDiff(diff.text),111 toolUseId: ctx.callId,112 },113 });114115 const newLines = countContentLines(input.content);116 let content: string;117 if (created) {118 content = `Wrote ${abs} (${newLines} lines).`;119 } else {120 const oldLines = countContentLines(oldContent ?? "");121 content = `Replaced ${abs} (was ${oldLines} lines, now ${newLines} lines).`;122 }123124 // Mandatory header reminder (Absolute Rule #0) — new project source files only.125 if (created && requiresKhaelorHeader(cwd, abs) && !hasKhaelorHeader(input.content)) {126 content += `\n\n${HEADER_REMINDER_NOTE}`;127 }128129 return {130 content,131 metadata: {132 title: created133 ? `Write ${rel} · new file · ${newLines} lines`134 : `Write ${rel} · +${diff.additions} −${diff.deletions}`,135 diff: diff.text,136 additions: diff.additions,137 deletions: diff.deletions,138 durationMs: Date.now() - startedAt,139 extra: { created },140 },141 };142}143144/** Create the write tool definition. */145export function createWriteTool(): ToolDefinition<WriteInput> {146 return {147 name: "write",148 description: DESCRIPTION,149 capability: "file.write",150 inputSchema: {151 type: "object",152 properties: {153 file_path: {154 type: "string",155 description: "Path of the file to write (absolute preferred).",156 },157 content: {158 type: "string",159 description: "The complete new content of the file.",160 },161 },162 required: ["file_path", "content"],163 },164 execute: executeWrite,165 };166}167