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,177 @@
|
|
|
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
|
+
// Search utility function
|
|
47
|
+
async function searchInFile(filePath, searchTerm, caseSensitive = false) {
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
const matchingLines = [];
|
|
52
|
+
let totalMatches = 0;
|
|
53
|
+
const searchPattern = caseSensitive ? searchTerm : searchTerm.toLowerCase();
|
|
54
|
+
lines.forEach((line, index) => {
|
|
55
|
+
const searchLine = caseSensitive ? line : line.toLowerCase();
|
|
56
|
+
if (searchLine.includes(searchPattern)) {
|
|
57
|
+
totalMatches++;
|
|
58
|
+
matchingLines.push(`${index + 1}: ${line}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return { matches: totalMatches, lines: matchingLines };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { matches: 0, lines: [] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function searchInDirectory(dirPath, searchTerm, caseSensitive = false, fileExtensions = []) {
|
|
68
|
+
const validPath = await validatePath(dirPath);
|
|
69
|
+
const results = [];
|
|
70
|
+
async function searchRecursive(currentPath, depth = 0) {
|
|
71
|
+
if (depth > 10)
|
|
72
|
+
return; // Prevent infinite recursion
|
|
73
|
+
try {
|
|
74
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
77
|
+
// Skip common directories that should be ignored
|
|
78
|
+
if (entry.isDirectory() && ['node_modules', '.git', '.DS_Store', 'dist', 'build'].includes(entry.name)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
await searchRecursive(fullPath, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
else if (entry.isFile()) {
|
|
85
|
+
// Check file extension filter
|
|
86
|
+
if (fileExtensions.length > 0) {
|
|
87
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
88
|
+
if (!fileExtensions.includes(ext)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const searchResult = await searchInFile(fullPath, searchTerm, caseSensitive);
|
|
94
|
+
if (searchResult.matches > 0) {
|
|
95
|
+
const relativePath = path.relative(workingDirectory, fullPath);
|
|
96
|
+
results.push(`\nš ${relativePath} (${searchResult.matches} matches):`);
|
|
97
|
+
results.push(...searchResult.lines.map(line => ` ${line}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Skip files we can't read
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Skip directories we can't read
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await searchRecursive(validPath);
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
return `No matches found for "${searchTerm}" in ${dirPath}`;
|
|
113
|
+
}
|
|
114
|
+
return results.join('\n');
|
|
115
|
+
}
|
|
116
|
+
export async function searchFiles(searchTerm, dirPath = '.', caseSensitive = false, fileExtensions = []) {
|
|
117
|
+
try {
|
|
118
|
+
if (!searchTerm.trim()) {
|
|
119
|
+
throw new Error('Search term cannot be empty');
|
|
120
|
+
}
|
|
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('Search path must be a directory');
|
|
126
|
+
}
|
|
127
|
+
const results = await searchInDirectory(validPath, searchTerm, caseSensitive, fileExtensions);
|
|
128
|
+
const searchInfo = [
|
|
129
|
+
`š Searching for: "${searchTerm}"`,
|
|
130
|
+
`š In directory: ${path.relative(workingDirectory, validPath) || '.'}`,
|
|
131
|
+
`š Case sensitive: ${caseSensitive ? 'Yes' : 'No'}`,
|
|
132
|
+
fileExtensions.length > 0 ? `šÆ File types: ${fileExtensions.join(', ')}` : 'šÆ All file types',
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
return searchInfo + results;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
throw new Error(`Failed to search files: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
throw new Error('Failed to search files: Unknown error');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Tool definition
|
|
145
|
+
export const searchFilesTool = {
|
|
146
|
+
type: 'function',
|
|
147
|
+
function: {
|
|
148
|
+
name: 'search_files',
|
|
149
|
+
description: 'Search for a text string within files in a directory. Supports case sensitivity and file type filtering. Returns the list of files and matching lines. Use this to find specific code, configuration settings, or documentation within your project files.',
|
|
150
|
+
parameters: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
search_term: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
description: 'The text string to search for within files.'
|
|
156
|
+
},
|
|
157
|
+
directory_path: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: 'The directory to search within, relative to the current working directory. Use "." for the current directory.'
|
|
160
|
+
},
|
|
161
|
+
case_sensitive: {
|
|
162
|
+
type: 'boolean',
|
|
163
|
+
description: 'Whether the search should be case sensitive.'
|
|
164
|
+
},
|
|
165
|
+
file_extensions: {
|
|
166
|
+
type: 'array',
|
|
167
|
+
items: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'File extension to include in the search (e.g., "js", "txt"). Leave empty to search all file types.'
|
|
170
|
+
},
|
|
171
|
+
description: 'List of file extensions to include in the search.'
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
required: ['search_term', 'directory_path']
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
// Search utility function
|
|
47
|
+
async function searchInFile(filePath, searchTerm, caseSensitive = false) {
|
|
48
|
+
try {
|
|
49
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
const matchingLines = [];
|
|
52
|
+
let totalMatches = 0;
|
|
53
|
+
const searchPattern = caseSensitive ? searchTerm : searchTerm.toLowerCase();
|
|
54
|
+
lines.forEach((line, index) => {
|
|
55
|
+
const searchLine = caseSensitive ? line : line.toLowerCase();
|
|
56
|
+
if (searchLine.includes(searchPattern)) {
|
|
57
|
+
totalMatches++;
|
|
58
|
+
matchingLines.push(`${index + 1}: ${line}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return { matches: totalMatches, lines: matchingLines };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { matches: 0, lines: [] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function searchInDirectory(dirPath, searchTerm, caseSensitive = false, fileExtensions = []) {
|
|
68
|
+
const validPath = await validatePath(dirPath);
|
|
69
|
+
const results = [];
|
|
70
|
+
async function searchRecursive(currentPath, depth = 0) {
|
|
71
|
+
if (depth > 10)
|
|
72
|
+
return; // Prevent infinite recursion
|
|
73
|
+
try {
|
|
74
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
77
|
+
// Skip common directories that should be ignored
|
|
78
|
+
if (entry.isDirectory() && ['node_modules', '.git', '.DS_Store', 'dist', 'build'].includes(entry.name)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
await searchRecursive(fullPath, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
else if (entry.isFile()) {
|
|
85
|
+
// Check file extension filter
|
|
86
|
+
if (fileExtensions.length > 0) {
|
|
87
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
88
|
+
if (!fileExtensions.includes(ext)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const searchResult = await searchInFile(fullPath, searchTerm, caseSensitive);
|
|
94
|
+
if (searchResult.matches > 0) {
|
|
95
|
+
const relativePath = path.relative(workingDirectory, fullPath);
|
|
96
|
+
results.push(`\nš ${relativePath} (${searchResult.matches} matches):`);
|
|
97
|
+
results.push(...searchResult.lines.map(line => ` ${line}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Skip files we can't read
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Skip directories we can't read
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await searchRecursive(validPath);
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
return `No matches found for "${searchTerm}" in ${dirPath}`;
|
|
113
|
+
}
|
|
114
|
+
return results.join('\n');
|
|
115
|
+
}
|
|
116
|
+
export async function searchFiles(searchTerm, dirPath = '.', caseSensitive = false, fileExtensions = []) {
|
|
117
|
+
try {
|
|
118
|
+
if (!searchTerm.trim()) {
|
|
119
|
+
throw new Error('Search term cannot be empty');
|
|
120
|
+
}
|
|
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('Search path must be a directory');
|
|
126
|
+
}
|
|
127
|
+
const results = await searchInDirectory(validPath, searchTerm, caseSensitive, fileExtensions);
|
|
128
|
+
const searchInfo = [
|
|
129
|
+
`š Searching for: "${searchTerm}"`,
|
|
130
|
+
`š In directory: ${path.relative(workingDirectory, validPath) || '.'}`,
|
|
131
|
+
`š Case sensitive: ${caseSensitive ? 'Yes' : 'No'}`,
|
|
132
|
+
fileExtensions.length > 0 ? `šÆ File types: ${fileExtensions.join(', ')}` : 'šÆ All file types',
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
return searchInfo + results;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
throw new Error(`Failed to search files: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
throw new Error('Failed to search files: Unknown error');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Tool definitions
|
|
145
|
+
export const searchTools = [
|
|
146
|
+
{
|
|
147
|
+
type: 'function',
|
|
148
|
+
function: {
|
|
149
|
+
name: 'search_files',
|
|
150
|
+
description: 'Search for a text string within files in a directory. Supports case sensitivity and file type filtering. Returns the list of files and matching lines. Use this to find specific code, configuration settings, or documentation within your project files.',
|
|
151
|
+
parameters: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
search_term: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'The text string to search for within files.'
|
|
157
|
+
},
|
|
158
|
+
directory_path: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'The directory to search within, relative to the current working directory. Use "." for the current directory.'
|
|
161
|
+
},
|
|
162
|
+
case_sensitive: {
|
|
163
|
+
type: 'boolean',
|
|
164
|
+
description: 'Whether the search should be case sensitive.'
|
|
165
|
+
},
|
|
166
|
+
file_extensions: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
items: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
description: 'File extension to include in the search (e.g., "js", "txt"). Leave empty to search all file types.'
|
|
171
|
+
},
|
|
172
|
+
description: 'List of file extensions to include in the search.'
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
required: ['search_term', 'directory_path']
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
];
|