/** * FileWriteTool — rewritten for reliability. * * BUG FIX: The "Unterminated string in JSON" errors were NOT from this file. * They were from the AI's streamed tool_calls getting truncated at 180s, * producing incomplete JSON like {"content":"... with no closing quote. * * This tool still handles edge cases better now: * 1. Validates content is a string before writing * 2. Auto-truncates extremely large content (>5MB) with a warning * 3. Better error messages that distinguish JSON parse vs filesystem errors */ import { logger } from '../utils/logger.js'; import fs from 'fs-extra'; import path from 'path'; export class FileWriteTool { constructor() { this.name = 'file_write'; this.description = 'Write content to a file, creating parent directories as needed'; } async execute(args) { // ── Input validation ── if (!args || typeof args !== 'object') { return '❌ file_write: Invalid arguments. Expected { file_path, content }.'; } const { file_path, content } = args; if (!file_path || typeof file_path !== 'string') { return '❌ file_write: file_path is required and must be a string.'; } if (content === undefined || content === null) { return '❌ file_write: content is required.'; } // If content is not a string (e.g., object from truncated JSON), stringify it let contentStr; if (typeof content === 'string') { contentStr = content; } else { contentStr = JSON.stringify(content); } // ── Size check ── const byteLength = Buffer.byteLength(contentStr); if (byteLength > 5 * 1024 * 1024) { logger.warn(`⚠ file_write: ${byteLength} bytes is very large for direct write, consider bash heredoc`); return `⚠ Warning: ${Math.round(byteLength / 1024)}KB — consider using bash with heredoc for large files: bash({ command: "cat > ${file_path} << 'EOF'\n...\nEOF" })`; } try { const fullPath = path.resolve(file_path); await fs.ensureDir(path.dirname(fullPath)); await fs.writeFile(fullPath, contentStr, 'utf-8'); logger.info(`✅ file_write: ${fullPath} (${Math.round(byteLength / 1024)}KB)`); return `✅ Written ${byteLength} bytes to ${fullPath}`; } catch (e) { // Distinguish filesystem errors from other issues if (e.code === 'EACCES') { return `❌ Permission denied: ${fullPath}. Check file permissions.`; } if (e.code === 'ENOSPC') { return `❌ Disk full: no space left on device.`; } logger.error(`❌ file_write failed: ${e.message}`); return `❌ Write error: ${e.message}`; } } }