/** * write — built-in tool to create/overwrite files (ARCHITECTURE §4.1). * * Writes `content` to `path` (resolved relative to cwd), creating parent * directories as needed. Overwrites existing files. Returns a status message * with byte count on success, or an `isError` result on permission/disk failure. * Never throws across the seam. */ import { Type, type Static } from '@sinclair/typebox'; import { Value } from '@sinclair/typebox/value'; import { resolve, dirname } from 'node:path'; import { mkdir } from 'node:fs/promises'; import type { Tool, ToolContext, ToolResult } from '@/contracts/tool'; import type { RenderDescriptor } from '@/contracts/render-descriptor'; // ── Input schema ────────────────────────────────────────────────────────────── export const WriteInputSchema = Type.Object({ path: Type.String({ description: 'File path, resolved relative to cwd' }), content: Type.String({ description: 'Content to write to the file' }), }); export type WriteInput = Static; // ── Tool definition ─────────────────────────────────────────────────────────── export const writeTool: Tool = { name: 'write', description: 'Write content to a file. Creates parent directories automatically. Overwrites if the file exists.', inputSchema: WriteInputSchema, isReadOnly: false, async execute(input: WriteInput, ctx: ToolContext): Promise { // Validate input against schema if (!Value.Check(WriteInputSchema, input)) { return { content: { kind: 'error', message: `Invalid input for write tool: ${JSON.stringify(input)}` }, isError: true, }; } const filePath = resolve(ctx.cwd, input.path); try { // Ensure parent directory exists await mkdir(dirname(filePath), { recursive: true }); await Bun.write(filePath, input.content); const byteLength = new TextEncoder().encode(input.content).length; return { content: { kind: 'text', text: `wrote ${byteLength} bytes to ${input.path}` }, isError: false, // duoh76.6: absolute path for the FileMutationNotifier. The error paths below // don't set this (no mutation happened). mutatedFiles: [filePath], }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { content: { kind: 'error', message: `Failed to write ${filePath}: ${message}` }, isError: true, }; } }, onStart(input: WriteInput): RenderDescriptor { return { kind: 'status', label: `writing ${input.path}`, tone: 'info' }; }, onResult(input: WriteInput, result: ToolResult): RenderDescriptor { if (result.isError) { const content = result.content as { kind: 'error'; message: string }; return { kind: 'status', label: `error writing ${input.path}: ${content.message}`, tone: 'error' }; } return { kind: 'status', label: `wrote ${input.path}`, tone: 'info' }; }, };