SPB Git

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%
5.5 KB · 167 lines typescript
Raw Blame History
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