protoagent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/agentic-loop.js +277 -0
- package/dist/config/client.js +166 -0
- package/dist/config/commands.js +208 -0
- package/dist/config/manager.js +117 -0
- package/dist/config/mcp-commands.js +266 -0
- package/dist/config/mcp-manager.js +240 -0
- package/dist/config/mcp-types.js +28 -0
- package/dist/config/providers.js +170 -0
- package/dist/config/setup.js +175 -0
- package/dist/config/system-prompt.js +301 -0
- package/dist/config/types.js +4 -0
- package/dist/index.js +156 -0
- package/dist/tools/create-directory.js +76 -0
- package/dist/tools/directory-operations.js +195 -0
- package/dist/tools/edit-file.js +144 -0
- package/dist/tools/file-operations.js +211 -0
- package/dist/tools/index.js +95 -0
- package/dist/tools/list-directory.js +84 -0
- package/dist/tools/read-file.js +111 -0
- package/dist/tools/run-shell-command.js +340 -0
- package/dist/tools/search-files.js +177 -0
- package/dist/tools/search-operations.js +179 -0
- package/dist/tools/shell-operations.js +342 -0
- package/dist/tools/todo.js +177 -0
- package/dist/tools/view-directory-tree.js +125 -0
- package/dist/tools/write-file.js +136 -0
- package/dist/tools.js +2 -0
- package/dist/utils/conversation-compactor.js +139 -0
- package/dist/utils/cost-tracker.js +106 -0
- package/dist/utils/file-operations-approval.js +163 -0
- package/dist/utils/logger.js +149 -0
- package/package.json +61 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// Current working directory for file operations
|
|
4
|
+
const workingDirectory = process.cwd();
|
|
5
|
+
// Security utilities
|
|
6
|
+
function normalizePath(p) {
|
|
7
|
+
return path.normalize(p);
|
|
8
|
+
}
|
|
9
|
+
async function validatePath(requestedPath) {
|
|
10
|
+
const absolute = path.isAbsolute(requestedPath)
|
|
11
|
+
? path.resolve(requestedPath)
|
|
12
|
+
: path.resolve(workingDirectory, requestedPath);
|
|
13
|
+
const normalizedRequested = normalizePath(absolute);
|
|
14
|
+
// Check if path is within working directory
|
|
15
|
+
if (!normalizedRequested.startsWith(workingDirectory)) {
|
|
16
|
+
throw new Error(`Access denied - path outside working directory: ${absolute}`);
|
|
17
|
+
}
|
|
18
|
+
// Handle symlinks by checking their real path
|
|
19
|
+
try {
|
|
20
|
+
const realPath = await fs.realpath(absolute);
|
|
21
|
+
const normalizedReal = normalizePath(realPath);
|
|
22
|
+
if (!normalizedReal.startsWith(workingDirectory)) {
|
|
23
|
+
throw new Error(`Access denied - symlink target outside working directory: ${realPath}`);
|
|
24
|
+
}
|
|
25
|
+
return realPath;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// For new files that don't exist yet, verify parent directory
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
const parentDir = path.dirname(absolute);
|
|
31
|
+
try {
|
|
32
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
33
|
+
const normalizedParent = normalizePath(realParentPath);
|
|
34
|
+
if (!normalizedParent.startsWith(workingDirectory)) {
|
|
35
|
+
throw new Error(`Access denied - parent directory outside working directory: ${realParentPath}`);
|
|
36
|
+
}
|
|
37
|
+
return absolute;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Directory tree utility function
|
|
47
|
+
async function buildDirectoryTree(dirPath, prefix = '', isLast = true, maxDepth = 5, currentDepth = 0) {
|
|
48
|
+
if (currentDepth >= maxDepth) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
const validPath = await validatePath(dirPath);
|
|
52
|
+
const stats = await fs.stat(validPath);
|
|
53
|
+
if (!stats.isDirectory()) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
let result = '';
|
|
57
|
+
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
58
|
+
// Sort entries: directories first, then files, alphabetically
|
|
59
|
+
entries.sort((a, b) => {
|
|
60
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
61
|
+
return -1;
|
|
62
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
63
|
+
return 1;
|
|
64
|
+
return a.name.localeCompare(b.name);
|
|
65
|
+
});
|
|
66
|
+
for (let i = 0; i < entries.length; i++) {
|
|
67
|
+
const entry = entries[i];
|
|
68
|
+
const isLastEntry = i === entries.length - 1;
|
|
69
|
+
const currentPrefix = isLastEntry ? '└── ' : '├── ';
|
|
70
|
+
const nextPrefix = prefix + (isLastEntry ? ' ' : '│ ');
|
|
71
|
+
result += `${prefix}${currentPrefix}${entry.name}${entry.isDirectory() ? '/' : ''}\n`;
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
const subPath = path.join(dirPath, entry.name);
|
|
74
|
+
try {
|
|
75
|
+
result += await buildDirectoryTree(subPath, nextPrefix, isLastEntry, maxDepth, currentDepth + 1);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Skip directories we can't read (permissions, etc.)
|
|
79
|
+
result += `${nextPrefix}└── [Permission Denied]\n`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
export async function listDirectory(dirPath) {
|
|
86
|
+
try {
|
|
87
|
+
const validPath = await validatePath(dirPath);
|
|
88
|
+
// Check if path exists and is a directory
|
|
89
|
+
const stats = await fs.stat(validPath);
|
|
90
|
+
if (!stats.isDirectory()) {
|
|
91
|
+
throw new Error('Path is not a directory');
|
|
92
|
+
}
|
|
93
|
+
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
94
|
+
const formatted = entries
|
|
95
|
+
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
return formatted || "Directory is empty";
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
throw new Error(`Failed to list directory: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
throw new Error('Failed to list directory: Unknown error');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function createDirectory(dirPath) {
|
|
107
|
+
try {
|
|
108
|
+
const validPath = await validatePath(dirPath);
|
|
109
|
+
await fs.mkdir(validPath, { recursive: true });
|
|
110
|
+
return `Successfully created directory ${dirPath}`;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
throw new Error(`Failed to create directory: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
throw new Error('Failed to create directory: Unknown error');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function viewDirectoryTree(dirPath, maxDepth = 5) {
|
|
120
|
+
try {
|
|
121
|
+
const validPath = await validatePath(dirPath);
|
|
122
|
+
// Check if path exists and is a directory
|
|
123
|
+
const stats = await fs.stat(validPath);
|
|
124
|
+
if (!stats.isDirectory()) {
|
|
125
|
+
throw new Error('Path is not a directory');
|
|
126
|
+
}
|
|
127
|
+
const relativePath = path.relative(workingDirectory, validPath) || '.';
|
|
128
|
+
const tree = await buildDirectoryTree(validPath, '', true, maxDepth);
|
|
129
|
+
return `Directory tree for ${relativePath}:\n${relativePath}/\n${tree}`;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
throw new Error(`Failed to view directory tree: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
throw new Error('Failed to view directory tree: Unknown error');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Tool definitions
|
|
139
|
+
export const directoryOperationTools = [
|
|
140
|
+
{
|
|
141
|
+
type: 'function',
|
|
142
|
+
function: {
|
|
143
|
+
name: 'list_directory',
|
|
144
|
+
description: 'List all files and directories in a specified directory. Use this to explore the project structure or find files. Returns a formatted list showing [FILE] and [DIR] prefixes.',
|
|
145
|
+
parameters: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
directory_path: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'The path to the directory to list, relative to the current working directory. Use "." for current directory. Examples: ".", "src", "src/components"'
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
required: ['directory_path']
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'function',
|
|
159
|
+
function: {
|
|
160
|
+
name: 'create_directory',
|
|
161
|
+
description: 'Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. Use this when you need to set up directory structures for projects.',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
directory_path: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The path to the directory to create, relative to the current working directory. Examples: "src/components", "tests/unit", "docs"'
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
required: ['directory_path']
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'function',
|
|
176
|
+
function: {
|
|
177
|
+
name: 'view_directory_tree',
|
|
178
|
+
description: 'Display the directory tree of a specified directory up to a certain depth. This provides a visual overview of the directory structure, showing nested folders and files. Useful for understanding project layout or finding files.',
|
|
179
|
+
parameters: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
directory_path: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'The path to the directory to display, relative to the current working directory. Examples: "src", "docs"'
|
|
185
|
+
},
|
|
186
|
+
max_depth: {
|
|
187
|
+
type: 'integer',
|
|
188
|
+
description: 'The maximum depth to display in the directory tree. Use a higher number to show more levels of the tree.'
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
required: ['directory_path']
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import { requestFileOperationApproval } from '../utils/file-operations-approval.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
// Current working directory for file operations
|
|
7
|
+
const workingDirectory = process.cwd();
|
|
8
|
+
// Security utilities
|
|
9
|
+
function normalizePath(p) {
|
|
10
|
+
return path.normalize(p);
|
|
11
|
+
}
|
|
12
|
+
async function validatePath(requestedPath) {
|
|
13
|
+
const absolute = path.isAbsolute(requestedPath)
|
|
14
|
+
? path.resolve(requestedPath)
|
|
15
|
+
: path.resolve(workingDirectory, requestedPath);
|
|
16
|
+
const normalizedRequested = normalizePath(absolute);
|
|
17
|
+
// Check if path is within working directory
|
|
18
|
+
if (!normalizedRequested.startsWith(workingDirectory)) {
|
|
19
|
+
throw new Error(`Access denied - path outside working directory: ${absolute}`);
|
|
20
|
+
}
|
|
21
|
+
// Handle symlinks by checking their real path
|
|
22
|
+
try {
|
|
23
|
+
const realPath = await fs.realpath(absolute);
|
|
24
|
+
const normalizedReal = normalizePath(realPath);
|
|
25
|
+
if (!normalizedReal.startsWith(workingDirectory)) {
|
|
26
|
+
throw new Error(`Access denied - symlink target outside working directory: ${realPath}`);
|
|
27
|
+
}
|
|
28
|
+
return realPath;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// For new files that don't exist yet, verify parent directory
|
|
32
|
+
if (error.code === 'ENOENT') {
|
|
33
|
+
const parentDir = path.dirname(absolute);
|
|
34
|
+
try {
|
|
35
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
36
|
+
const normalizedParent = normalizePath(realParentPath);
|
|
37
|
+
if (!normalizedParent.startsWith(workingDirectory)) {
|
|
38
|
+
throw new Error(`Access denied - parent directory outside working directory: ${realParentPath}`);
|
|
39
|
+
}
|
|
40
|
+
return absolute;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function editFile(filePath, oldString, newString, expectedReplacements) {
|
|
50
|
+
try {
|
|
51
|
+
const validPath = await validatePath(filePath);
|
|
52
|
+
// Read current file content
|
|
53
|
+
const currentContent = await fs.readFile(validPath, 'utf-8');
|
|
54
|
+
// Check if the old string exists in the file
|
|
55
|
+
if (!currentContent.includes(oldString)) {
|
|
56
|
+
throw new Error(`Old string not found in file. Make sure the text matches exactly, including whitespace and line breaks.`);
|
|
57
|
+
}
|
|
58
|
+
// Count occurrences of the old string
|
|
59
|
+
const occurrences = (currentContent.match(new RegExp(escapeRegExp(oldString), 'g')) || []).length;
|
|
60
|
+
// If expected replacements is specified, validate it
|
|
61
|
+
if (expectedReplacements !== undefined) {
|
|
62
|
+
if (occurrences !== expectedReplacements) {
|
|
63
|
+
throw new Error(`Expected ${expectedReplacements} occurrences of the old string, but found ${occurrences}. This prevents accidental multiple replacements.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (occurrences > 1) {
|
|
67
|
+
throw new Error(`Found ${occurrences} occurrences of the old string. Use expected_replacements parameter to confirm you want to replace all ${occurrences} occurrences, or make your old_string more specific to match only the intended text.`);
|
|
68
|
+
}
|
|
69
|
+
// Perform the replacement to show preview
|
|
70
|
+
const updatedContent = currentContent.replace(new RegExp(escapeRegExp(oldString), 'g'), newString);
|
|
71
|
+
// Verify that the replacement actually changed something
|
|
72
|
+
if (updatedContent === currentContent) {
|
|
73
|
+
throw new Error(`No changes were made to the file. The old_string and new_string appear to be identical.`);
|
|
74
|
+
}
|
|
75
|
+
// Request user approval for edit operation
|
|
76
|
+
const replacementCount = expectedReplacements || 1;
|
|
77
|
+
const approvalContext = {
|
|
78
|
+
operation: 'edit',
|
|
79
|
+
filePath: filePath,
|
|
80
|
+
description: `Replace ${replacementCount} occurrence${replacementCount === 1 ? '' : 's'} of text in file`,
|
|
81
|
+
contentPreview: `OLD TEXT (${oldString.length} chars):\n${oldString.substring(0, 200)}${oldString.length > 200 ? '...' : ''}\n\nNEW TEXT (${newString.length} chars):\n${newString.substring(0, 200)}${newString.length > 200 ? '...' : ''}`
|
|
82
|
+
};
|
|
83
|
+
const approved = await requestFileOperationApproval(approvalContext);
|
|
84
|
+
if (!approved) {
|
|
85
|
+
return `Edit operation cancelled by user: ${filePath}`;
|
|
86
|
+
}
|
|
87
|
+
logger.debug(`✏️ Editing file: ${filePath} (${replacementCount} replacement${replacementCount === 1 ? '' : 's'})`, { component: 'EditFile', operation: 'editFile' });
|
|
88
|
+
// Write the updated content using atomic operation
|
|
89
|
+
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
|
|
90
|
+
try {
|
|
91
|
+
await fs.writeFile(tempPath, updatedContent, 'utf-8');
|
|
92
|
+
await fs.rename(tempPath, validPath);
|
|
93
|
+
logger.info(`✅ Successfully edited: ${filePath} (${replacementCount} replacement${replacementCount === 1 ? '' : 's'})`, { component: 'EditFile', operation: 'editFile' });
|
|
94
|
+
}
|
|
95
|
+
catch (renameError) {
|
|
96
|
+
try {
|
|
97
|
+
await fs.unlink(tempPath);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
throw renameError;
|
|
101
|
+
}
|
|
102
|
+
return `Successfully edited ${filePath} - replaced ${replacementCount} occurrence${replacementCount === 1 ? '' : 's'} of the specified text`;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof Error) {
|
|
106
|
+
throw new Error(`Failed to edit file: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
throw new Error('Failed to edit file: Unknown error');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Helper function to escape special regex characters
|
|
112
|
+
function escapeRegExp(string) {
|
|
113
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
114
|
+
}
|
|
115
|
+
// Tool definition
|
|
116
|
+
export const editFileTool = {
|
|
117
|
+
type: 'function',
|
|
118
|
+
function: {
|
|
119
|
+
name: 'edit_file',
|
|
120
|
+
description: 'Edit an existing file by replacing specific text with new text. This operation is atomic and ensures file integrity. Use this to modify configuration files, scripts, or any text files where you need to update specific sections. The old_string must match exactly, including whitespace and line breaks.',
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
file_path: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'The path to the file to edit, relative to the current working directory. Examples: "src/config.json", "README.md"'
|
|
127
|
+
},
|
|
128
|
+
old_string: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'The exact text to find and replace. Must match exactly, including whitespace, indentation, and line breaks. For better precision, include some surrounding context.'
|
|
131
|
+
},
|
|
132
|
+
new_string: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'The new text to replace the old_string with. Can be empty string to delete text.'
|
|
135
|
+
},
|
|
136
|
+
expected_replacements: {
|
|
137
|
+
type: 'integer',
|
|
138
|
+
description: 'Optional: The expected number of replacements. If not specified, only allows 1 replacement. Use this to confirm when you want to replace multiple occurrences of the same text.'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
// Current working directory for file operations
|
|
5
|
+
const workingDirectory = process.cwd();
|
|
6
|
+
// Security utilities
|
|
7
|
+
function normalizePath(p) {
|
|
8
|
+
return path.normalize(p);
|
|
9
|
+
}
|
|
10
|
+
async function validatePath(requestedPath) {
|
|
11
|
+
const absolute = path.isAbsolute(requestedPath)
|
|
12
|
+
? path.resolve(requestedPath)
|
|
13
|
+
: path.resolve(workingDirectory, requestedPath);
|
|
14
|
+
const normalizedRequested = normalizePath(absolute);
|
|
15
|
+
// Check if path is within working directory
|
|
16
|
+
if (!normalizedRequested.startsWith(workingDirectory)) {
|
|
17
|
+
throw new Error(`Access denied - path outside working directory: ${absolute}`);
|
|
18
|
+
}
|
|
19
|
+
// Handle symlinks by checking their real path
|
|
20
|
+
try {
|
|
21
|
+
const realPath = await fs.realpath(absolute);
|
|
22
|
+
const normalizedReal = normalizePath(realPath);
|
|
23
|
+
if (!normalizedReal.startsWith(workingDirectory)) {
|
|
24
|
+
throw new Error(`Access denied - symlink target outside working directory: ${realPath}`);
|
|
25
|
+
}
|
|
26
|
+
return realPath;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// For new files that don't exist yet, verify parent directory
|
|
30
|
+
if (error.code === 'ENOENT') {
|
|
31
|
+
const parentDir = path.dirname(absolute);
|
|
32
|
+
try {
|
|
33
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
34
|
+
const normalizedParent = normalizePath(realParentPath);
|
|
35
|
+
if (!normalizedParent.startsWith(workingDirectory)) {
|
|
36
|
+
throw new Error(`Access denied - parent directory outside working directory: ${realParentPath}`);
|
|
37
|
+
}
|
|
38
|
+
return absolute;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function readFile(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
const validPath = await validatePath(filePath);
|
|
50
|
+
// Check if file exists and is actually a file
|
|
51
|
+
const stats = await fs.stat(validPath);
|
|
52
|
+
if (!stats.isFile()) {
|
|
53
|
+
throw new Error('Path is not a file');
|
|
54
|
+
}
|
|
55
|
+
// Read and return file content
|
|
56
|
+
const content = await fs.readFile(validPath, 'utf-8');
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
throw new Error('Failed to read file: Unknown error');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function writeFile(filePath, content) {
|
|
67
|
+
try {
|
|
68
|
+
const validPath = await validatePath(filePath);
|
|
69
|
+
try {
|
|
70
|
+
// Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
|
|
71
|
+
// preventing writes through pre-existing symlinks
|
|
72
|
+
await fs.writeFile(validPath, content, { encoding: "utf-8", flag: 'wx' });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === 'EEXIST') {
|
|
76
|
+
// Security: Use atomic rename to prevent race conditions where symlinks
|
|
77
|
+
// could be created between validation and write. Rename operations
|
|
78
|
+
// replace the target file atomically and don't follow symlinks.
|
|
79
|
+
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
82
|
+
await fs.rename(tempPath, validPath);
|
|
83
|
+
}
|
|
84
|
+
catch (renameError) {
|
|
85
|
+
try {
|
|
86
|
+
await fs.unlink(tempPath);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
throw renameError;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return `Successfully wrote to ${filePath}`;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
throw new Error(`Failed to write file: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
throw new Error('Failed to write file: Unknown error');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function editFile(filePath, startLine, endLine, newContent) {
|
|
106
|
+
try {
|
|
107
|
+
const validPath = await validatePath(filePath);
|
|
108
|
+
// Read current file content
|
|
109
|
+
const currentContent = await fs.readFile(validPath, 'utf-8');
|
|
110
|
+
const lines = currentContent.split('\n');
|
|
111
|
+
// Validate line numbers (1-based indexing for user, 0-based for array)
|
|
112
|
+
if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length || startLine > endLine) {
|
|
113
|
+
throw new Error(`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`);
|
|
114
|
+
}
|
|
115
|
+
// Replace the specified lines
|
|
116
|
+
const newLines = newContent.split('\n');
|
|
117
|
+
const beforeLines = lines.slice(0, startLine - 1);
|
|
118
|
+
const afterLines = lines.slice(endLine);
|
|
119
|
+
const updatedContent = [...beforeLines, ...newLines, ...afterLines].join('\n');
|
|
120
|
+
// Write the updated content using atomic operation
|
|
121
|
+
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
|
|
122
|
+
try {
|
|
123
|
+
await fs.writeFile(tempPath, updatedContent, 'utf-8');
|
|
124
|
+
await fs.rename(tempPath, validPath);
|
|
125
|
+
}
|
|
126
|
+
catch (renameError) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.unlink(tempPath);
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
throw renameError;
|
|
132
|
+
}
|
|
133
|
+
return `Successfully edited ${filePath} (lines ${startLine}-${endLine})`;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
throw new Error(`Failed to edit file: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
throw new Error('Failed to edit file: Unknown error');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Tool definitions
|
|
143
|
+
export const fileOperationTools = [
|
|
144
|
+
{
|
|
145
|
+
type: 'function',
|
|
146
|
+
function: {
|
|
147
|
+
name: 'read_file',
|
|
148
|
+
description: 'Read the contents of a file in the current working directory or its subdirectories. Use this when you need to examine code, configuration files, or any text-based files to help the user.',
|
|
149
|
+
parameters: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
file_path: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: 'The path to the file to read, relative to the current working directory. Examples: "src/index.ts", "package.json", "README.md"'
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
required: ['file_path']
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'function',
|
|
163
|
+
function: {
|
|
164
|
+
name: 'write_file',
|
|
165
|
+
description: 'Create a new file or completely overwrite an existing file with new content. Use this when you need to create new files or update existing ones. The file will be created in the current working directory or subdirectories.',
|
|
166
|
+
parameters: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
file_path: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'The path to the file to write, relative to the current working directory. Examples: "src/newfile.ts", "config.json", "README.md"'
|
|
172
|
+
},
|
|
173
|
+
content: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'The content to write to the file. This will completely replace any existing content.'
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
required: ['file_path', 'content']
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: 'function',
|
|
184
|
+
function: {
|
|
185
|
+
name: 'edit_file',
|
|
186
|
+
description: 'Edit an existing file by replacing specific lines with new content. This operation is atomic and ensures file integrity. Use this to modify configuration files, scripts, or any text files where you need to update specific sections.',
|
|
187
|
+
parameters: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
file_path: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'The path to the file to edit, relative to the current working directory. Examples: "src/config.json", "README.md"'
|
|
193
|
+
},
|
|
194
|
+
start_line: {
|
|
195
|
+
type: 'integer',
|
|
196
|
+
description: 'The starting line number for the replacement (inclusive, 1-based).'
|
|
197
|
+
},
|
|
198
|
+
end_line: {
|
|
199
|
+
type: 'integer',
|
|
200
|
+
description: 'The ending line number for the replacement (inclusive, 1-based).'
|
|
201
|
+
},
|
|
202
|
+
new_content: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'The new content to insert, replacing the specified line range.'
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
required: ['file_path', 'start_line', 'end_line', 'new_content']
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Export all tools and their implementations
|
|
2
|
+
export { readFile, readFileTool } from './read-file.js';
|
|
3
|
+
export { writeFile, writeFileTool } from './write-file.js';
|
|
4
|
+
export { editFile, editFileTool } from './edit-file.js';
|
|
5
|
+
export { listDirectory, listDirectoryTool } from './list-directory.js';
|
|
6
|
+
export { createDirectory, createDirectoryTool } from './create-directory.js';
|
|
7
|
+
export { viewDirectoryTree, viewDirectoryTreeTool } from './view-directory-tree.js';
|
|
8
|
+
export { searchFiles, searchFilesTool } from './search-files.js';
|
|
9
|
+
export { runShellCommand, setShellConfig, setDangerouslyAcceptAll, runShellCommandTool } from './run-shell-command.js';
|
|
10
|
+
export { handleTodoTool, todoReadTool, todoWriteTool } from './todo.js';
|
|
11
|
+
export { setFileOperationConfig, setDangerouslyAcceptAllFileOps, getSessionApprovalStatus, clearSessionApprovals, showApprovalStatus } from '../utils/file-operations-approval.js';
|
|
12
|
+
// Import all tool definitions
|
|
13
|
+
import { readFileTool } from './read-file.js';
|
|
14
|
+
import { writeFileTool } from './write-file.js';
|
|
15
|
+
import { editFileTool } from './edit-file.js';
|
|
16
|
+
import { listDirectoryTool } from './list-directory.js';
|
|
17
|
+
import { createDirectoryTool } from './create-directory.js';
|
|
18
|
+
import { viewDirectoryTreeTool } from './view-directory-tree.js';
|
|
19
|
+
import { searchFilesTool } from './search-files.js';
|
|
20
|
+
import { runShellCommandTool } from './run-shell-command.js';
|
|
21
|
+
import { todoReadTool, todoWriteTool } from './todo.js';
|
|
22
|
+
// Import all tool implementations
|
|
23
|
+
import { readFile } from './read-file.js';
|
|
24
|
+
import { writeFile } from './write-file.js';
|
|
25
|
+
import { editFile } from './edit-file.js';
|
|
26
|
+
import { listDirectory } from './list-directory.js';
|
|
27
|
+
import { createDirectory } from './create-directory.js';
|
|
28
|
+
import { viewDirectoryTree } from './view-directory-tree.js';
|
|
29
|
+
import { searchFiles } from './search-files.js';
|
|
30
|
+
import { runShellCommand } from './run-shell-command.js';
|
|
31
|
+
import { handleTodoTool } from './todo.js';
|
|
32
|
+
// Consolidated tool definitions
|
|
33
|
+
export const tools = [
|
|
34
|
+
readFileTool,
|
|
35
|
+
writeFileTool,
|
|
36
|
+
editFileTool,
|
|
37
|
+
listDirectoryTool,
|
|
38
|
+
createDirectoryTool,
|
|
39
|
+
viewDirectoryTreeTool,
|
|
40
|
+
searchFilesTool,
|
|
41
|
+
runShellCommandTool,
|
|
42
|
+
todoReadTool,
|
|
43
|
+
todoWriteTool
|
|
44
|
+
];
|
|
45
|
+
// Tool handler function
|
|
46
|
+
export async function handleToolCall(toolName, args) {
|
|
47
|
+
try {
|
|
48
|
+
switch (toolName) {
|
|
49
|
+
case 'read_file':
|
|
50
|
+
const content = await readFile(args.file_path, args.offset, args.limit);
|
|
51
|
+
return `File content of ${args.file_path}:\n\n${content}`;
|
|
52
|
+
case 'write_file':
|
|
53
|
+
const writeResult = await writeFile(args.file_path, args.content);
|
|
54
|
+
return writeResult;
|
|
55
|
+
case 'edit_file':
|
|
56
|
+
const editResult = await editFile(args.file_path, args.old_string, args.new_string, args.expected_replacements);
|
|
57
|
+
return editResult;
|
|
58
|
+
case 'list_directory':
|
|
59
|
+
const listResult = await listDirectory(args.directory_path);
|
|
60
|
+
return `Contents of ${args.directory_path}:\n\n${listResult}`;
|
|
61
|
+
case 'create_directory':
|
|
62
|
+
const createResult = await createDirectory(args.directory_path);
|
|
63
|
+
return createResult;
|
|
64
|
+
case 'view_directory_tree':
|
|
65
|
+
const treeResult = await viewDirectoryTree(args.directory_path, args.max_depth);
|
|
66
|
+
return treeResult;
|
|
67
|
+
case 'search_files':
|
|
68
|
+
const searchResult = await searchFiles(args.search_term, args.directory_path, args.case_sensitive, args.file_extensions);
|
|
69
|
+
return searchResult;
|
|
70
|
+
case 'run_shell_command':
|
|
71
|
+
const shellResult = await runShellCommand(args.command, args.args || [], args.timeout_ms || 30000);
|
|
72
|
+
return shellResult;
|
|
73
|
+
case 'todo_read':
|
|
74
|
+
case 'todo_write':
|
|
75
|
+
return await handleTodoTool(toolName, args);
|
|
76
|
+
default:
|
|
77
|
+
return `Error: Unknown tool ${toolName}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
82
|
+
return `Error executing ${toolName}: ${errorMessage}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Convenience functions for config setup
|
|
86
|
+
import { setShellConfig, setDangerouslyAcceptAll } from './run-shell-command.js';
|
|
87
|
+
import { setFileOperationConfig, setDangerouslyAcceptAllFileOps } from '../utils/file-operations-approval.js';
|
|
88
|
+
export function setToolsConfig(config) {
|
|
89
|
+
setShellConfig(config);
|
|
90
|
+
setFileOperationConfig(config);
|
|
91
|
+
}
|
|
92
|
+
export function setToolsDangerouslyAcceptAll(accept) {
|
|
93
|
+
setDangerouslyAcceptAll(accept);
|
|
94
|
+
setDangerouslyAcceptAllFileOps(accept);
|
|
95
|
+
}
|