capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
132 lines • 4.88 kB
JavaScript
import { readFile, writeFile } from 'fs/promises';
import path from 'path';
import { BaseTool } from '../base.js';
export class FileEditTool extends BaseTool {
name = 'file_edit';
displayName = '✏️ Edit File';
description = 'Edit a file by replacing specific text content';
category = 'file';
icon = '✏️';
parameters = [
{
name: 'path',
type: 'string',
description: 'Path to the file to edit (absolute or relative to working directory)',
required: true
},
{
name: 'oldText',
type: 'string',
description: 'Text to find and replace (must match exactly)',
required: true
},
{
name: 'newText',
type: 'string',
description: 'Text to replace with',
required: true
},
{
name: 'replaceAll',
type: 'boolean',
description: 'Replace all occurrences (default: false - only first)',
default: false
}
];
permissions = {
fileSystem: 'write'
};
ui = {
showProgress: false,
collapsible: true,
dangerous: true
};
async run(params, context) {
const { path: filePath, oldText, newText, replaceAll = false } = params;
if (oldText === newText) {
throw new Error('Old text and new text must be different');
}
const resolvedPath = path.isAbsolute(filePath)
? filePath
: path.join(context.workingDirectory || process.cwd(), filePath);
this.reportProgress(context, `Reading file: ${resolvedPath}`);
try {
const content = await readFile(resolvedPath, 'utf8');
if (!content.includes(oldText)) {
throw new Error(`Text not found in file: "${oldText.substring(0, 50)}${oldText.length > 50 ? '...' : ''}"`);
}
const occurrences = content.split(oldText).length - 1;
let newContent;
if (replaceAll) {
newContent = content.split(oldText).join(newText);
}
else {
const index = content.indexOf(oldText);
newContent = content.substring(0, index) + newText + content.substring(index + oldText.length);
}
this.reportProgress(context, `Writing changes to: ${resolvedPath}`);
await writeFile(resolvedPath, newContent, 'utf8');
const linesChanged = this.calculateLinesChanged(content, newContent);
const replacements = replaceAll ? occurrences : 1;
return {
path: resolvedPath,
replacements,
totalOccurrences: occurrences,
linesChanged,
preview: this.generatePreview(content, newContent, oldText, newText)
};
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`File not found: ${resolvedPath}`);
}
else if (error.code === 'EACCES') {
throw new Error(`Permission denied: ${resolvedPath}`);
}
throw error;
}
}
calculateLinesChanged(oldContent, newContent) {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
let changes = 0;
const maxLength = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLength; i++) {
if (oldLines[i] !== newLines[i]) {
changes++;
}
}
return changes;
}
generatePreview(oldContent, newContent, _oldText, _newText) {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
let firstChangeLine = -1;
for (let i = 0; i < oldLines.length; i++) {
if (oldLines[i] !== newLines[i]) {
firstChangeLine = i;
break;
}
}
if (firstChangeLine === -1)
return 'No changes';
const contextLines = 2;
const startLine = Math.max(0, firstChangeLine - contextLines);
const endLine = Math.min(newLines.length, firstChangeLine + contextLines + 1);
const preview = [];
for (let i = startLine; i < endLine; i++) {
const lineNum = (i + 1).toString().padStart(4, ' ');
if (i === firstChangeLine) {
if (oldLines[i]) {
preview.push(`${lineNum} - │ ${oldLines[i]}`);
}
preview.push(`${lineNum} + │ ${newLines[i]}`);
}
else {
preview.push(`${lineNum} │ ${newLines[i]}`);
}
}
return preview.join('\n');
}
}
//# sourceMappingURL=file-edit.js.map