UNPKG

mnemos-coder

Version:

CLI-based coding agent with graph-based execution loop and terminal UI

141 lines 5.58 kB
/** * Safe file editor tool: read/write/replace/diff/applyPatch with backups */ import path from 'path'; import { promises as fs } from 'fs'; import { createTwoFilesPatch, applyPatch } from 'diff'; export class FileEditor { workspace; defaultRequireBackup; constructor(workspace, defaultRequireBackup = true) { this.workspace = workspace; this.defaultRequireBackup = defaultRequireBackup; } async readFile(filePath) { const abs = this.resolvePath(filePath); try { return await fs.readFile(abs, 'utf-8'); } catch (err) { if (err && err.code === 'ENOENT') return ''; throw err; } } async writeFile(filePath, newContent, options = {}) { const abs = this.resolvePath(filePath); const oldContent = await this.readFile(filePath); if (oldContent === newContent) { const diff = { file_path: this.relativePath(abs), unified_diff: this.createUnifiedDiff(filePath, oldContent, newContent), applied: true }; return { changes: [diff], summary: `No changes needed for ${diff.file_path}` }; } const requireBackup = options.require_backup ?? this.defaultRequireBackup; let backupPath; if (requireBackup && oldContent) { backupPath = await this.backupFile(abs, oldContent, options); } if (!options.dry_run) { await this.ensureDir(path.dirname(abs)); await fs.writeFile(abs, newContent, 'utf-8'); } const change = { file_path: this.relativePath(abs), unified_diff: this.createUnifiedDiff(filePath, oldContent, newContent), applied: !options.dry_run, backup_path: backupPath }; return { changes: [change], summary: `Wrote ${change.file_path}` }; } async replaceInFile(filePath, search, replace, options = {}) { const content = await this.readFile(filePath); const newContent = typeof search === 'string' ? content.split(search).join(replace) : content.replace(search, replace); return this.writeFile(filePath, newContent, options); } async insertByMarker(filePath, marker, insertPosition, text, options = {}) { const content = await this.readFile(filePath); let index = -1; if (typeof marker === 'string') { index = content.indexOf(marker); } else { const match = content.match(marker); if (match && match.index !== undefined) index = match.index; } if (index < 0) { return this.writeFile(filePath, content + text, options); } const insertionIndex = insertPosition === 'before' ? index : index + (typeof marker === 'string' ? marker.length : 0); const newContent = content.slice(0, insertionIndex) + text + content.slice(insertionIndex); return this.writeFile(filePath, newContent, options); } async applyUnifiedDiff(filePath, unifiedDiff, options = {}) { const oldContent = await this.readFile(filePath); const patched = applyPatch(oldContent, unifiedDiff); if (patched === false) { throw new Error(`Failed to apply patch to ${filePath}`); } return this.writeFile(filePath, patched, options); } async deleteFile(filePath, options = {}) { const abs = this.resolvePath(filePath); const old = await this.readFile(filePath); let backupPath; const requireBackup = options.require_backup ?? this.defaultRequireBackup; if (requireBackup && old) { backupPath = await this.backupFile(abs, old, options); } if (!options.dry_run) { try { await fs.unlink(abs); } catch (err) { if (!(err && err.code === 'ENOENT')) throw err; } } const change = { file_path: this.relativePath(abs), unified_diff: this.createUnifiedDiff(filePath, old, ''), applied: !options.dry_run, backup_path: backupPath }; return { changes: [change], summary: `Deleted ${change.file_path}` }; } async ensureDir(dirPath) { await fs.mkdir(dirPath, { recursive: true }); } resolvePath(p) { if (path.isAbsolute(p)) return p; return path.join(this.workspace, p); } relativePath(abs) { return path.relative(this.workspace, abs) || abs; } createUnifiedDiff(filePath, oldContent, newContent) { const rel = this.relativePath(this.resolvePath(filePath)); return createTwoFilesPatch(rel, rel, oldContent, newContent, 'old', 'new'); } async backupFile(absPath, content, options) { const rel = this.relativePath(absPath); const stamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupAbs = path.join(this.workspace, '.mnemos-backups', stamp, rel); const backupDir = path.dirname(backupAbs); if (!options.dry_run) { await this.ensureDir(backupDir); await fs.writeFile(backupAbs, content, 'utf-8'); } return this.relativePath(backupAbs); } } export function createFileEditor(workspace, defaultRequireBackup = true) { return new FileEditor(workspace, defaultRequireBackup); } //# sourceMappingURL=FileEditor.js.map