Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/tools/descriptions/filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Rich descriptions for the file system tools (read_file, write_file, edit_file).
* Used in the system prompt to guide the LLM on when and how to use these tools.
*/

export const READ_FILE_DESCRIPTION = `
Read a file from the local file system. Returns content with line numbers.

## When to Use

- Reading source code, configuration files, or data files
- Inspecting file contents before making edits
- Reviewing project structure and code organization

## When NOT to Use

- Reading web pages or remote URLs (use web_fetch instead)
- Reading SEC filings (use read_filings instead)
- Listing directory contents (use a shell command instead)

## Schema

- **file_path** (required): Absolute or relative path to the file
- **offset** (optional): Line number to start reading from (0-based, default 0)
- **limit** (optional): Maximum number of lines to read (default 2000)

## Returns

File content with line numbers (cat -n style). Includes metadata about total lines and truncation.

## Usage Notes

- Always read a file before editing it to understand its current content
- Use offset and limit for large files to avoid overwhelming the context
- Lines longer than 2000 characters are truncated
- Requires file system permissions — see .dexter/permissions.json
`.trim();

export const WRITE_FILE_DESCRIPTION = `
Write content to a file on the local file system. Creates the file or overwrites it.

## When to Use

- Creating new files (scripts, configurations, data files)
- Overwriting a file with entirely new content
- Saving generated output to disk

## When NOT to Use

- Making small edits to an existing file (use edit_file instead — it's safer)
- Appending to a file (read first, then write the combined content)

## Schema

- **file_path** (required): Absolute or relative path to the file
- **content** (required): The content to write to the file

## Returns

Confirmation with file path and bytes written.

## Usage Notes

- Creates parent directories automatically if they don't exist
- Overwrites the file if it already exists — be careful with existing files
- Prefer edit_file for targeted changes to avoid accidentally losing content
- Requires file system permissions — see .dexter/permissions.json
`.trim();

export const EDIT_FILE_DESCRIPTION = `
Perform exact string replacement in a file. Finds old_string and replaces it with new_string.

## When to Use

- Making targeted edits to existing files
- Fixing bugs, updating values, or refactoring code
- Renaming variables or strings across a file (with replace_all)

## When NOT to Use

- Creating a new file from scratch (use write_file instead)
- Rewriting most of a file's content (use write_file instead)

## Schema

- **file_path** (required): Absolute or relative path to the file
- **old_string** (required): The exact text to find and replace
- **new_string** (required): The replacement text
- **replace_all** (optional): If true, replace all occurrences (default false)

## Returns

Confirmation with the number of replacements made.

## Usage Notes

- Always read the file first so you know the exact content to match
- old_string must match exactly, including whitespace and indentation
- By default, old_string must appear exactly once (prevents accidental mass edits)
- Use replace_all: true to replace all occurrences (e.g., renaming a variable)
- Requires file system permissions — see .dexter/permissions.json
`.trim();
1 change: 1 addition & 0 deletions src/tools/descriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { WEB_SEARCH_DESCRIPTION } from './web-search.js';
export { READ_FILINGS_DESCRIPTION } from './read-filings.js';
export { WEB_FETCH_DESCRIPTION } from './web-fetch.js';
export { BROWSER_DESCRIPTION } from './browser.js';
export { READ_FILE_DESCRIPTION, WRITE_FILE_DESCRIPTION, EDIT_FILE_DESCRIPTION } from './filesystem.js';
90 changes: 90 additions & 0 deletions src/tools/filesystem/edit-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* edit_file tool — performs exact string replacements in a file.
*
* Checks permissions before editing. Validates that old_string exists and
* is unique (unless replace_all is true).
*/
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { formatToolResult } from '../types.js';
import { requestPermission } from '../../utils/permissions.js';

export const editFileTool = new DynamicStructuredTool({
name: 'edit_file',
description:
'Perform exact string replacement in a file. Finds old_string in the file and replaces it with new_string. By default, old_string must appear exactly once (use replace_all for multiple occurrences).',
schema: z.object({
file_path: z.string().describe('Absolute or relative path to the file to edit.'),
old_string: z.string().describe('The exact text to find and replace.'),
new_string: z.string().describe('The replacement text.'),
replace_all: z
.boolean()
.optional()
.default(false)
.describe('If true, replace all occurrences. If false (default), old_string must be unique.'),
}),
func: async (input) => {
const filePath = resolve(input.file_path);

// Check permissions
const perm = requestPermission('edit_file', filePath);
if (!perm.allowed) {
return perm.message!;
}

if (!existsSync(filePath)) {
return `Error: File not found: ${filePath}`;
}

if (input.old_string === input.new_string) {
return 'Error: old_string and new_string are identical. No changes needed.';
}

try {
const content = readFileSync(filePath, 'utf-8');

// Count occurrences
let count = 0;
let idx = 0;
while ((idx = content.indexOf(input.old_string, idx)) !== -1) {
count++;
idx += input.old_string.length;
}

if (count === 0) {
return `Error: old_string not found in ${filePath}. Make sure the string matches exactly (including whitespace and indentation).`;
}

if (count > 1 && !input.replace_all) {
return (
`Error: old_string appears ${count} times in ${filePath}. ` +
`Provide more surrounding context to make it unique, or set replace_all to true.`
);
}

// Perform replacement
let newContent: string;
if (input.replace_all) {
newContent = content.split(input.old_string).join(input.new_string);
} else {
// Replace first (and only) occurrence
const pos = content.indexOf(input.old_string);
newContent =
content.slice(0, pos) + input.new_string + content.slice(pos + input.old_string.length);
}

writeFileSync(filePath, newContent, 'utf-8');

return formatToolResult({
file_path: filePath,
replacements: count,
message: `Successfully replaced ${count} occurrence${count !== 1 ? 's' : ''} in ${filePath}`,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error editing file: ${message}`;
}
},
});
Loading