mnemos-coder
Version:
CLI-based coding agent with graph-based execution loop and terminal UI
141 lines • 5.58 kB
JavaScript
/**
* 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