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,84 @@
|
|
|
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
|
+
export async function listDirectory(dirPath) {
|
|
47
|
+
try {
|
|
48
|
+
const validPath = await validatePath(dirPath);
|
|
49
|
+
// Check if path exists and is a directory
|
|
50
|
+
const stats = await fs.stat(validPath);
|
|
51
|
+
if (!stats.isDirectory()) {
|
|
52
|
+
throw new Error('Path is not a directory');
|
|
53
|
+
}
|
|
54
|
+
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
55
|
+
const formatted = entries
|
|
56
|
+
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
|
57
|
+
.join("\n");
|
|
58
|
+
return formatted || "Directory is empty";
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
throw new Error(`Failed to list directory: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Failed to list directory: Unknown error');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Tool definition
|
|
68
|
+
export const listDirectoryTool = {
|
|
69
|
+
type: 'function',
|
|
70
|
+
function: {
|
|
71
|
+
name: 'list_directory',
|
|
72
|
+
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.',
|
|
73
|
+
parameters: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
directory_path: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'The path to the directory to list, relative to the current working directory. Use "." for current directory. Examples: ".", "src", "src/components"'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
required: ['directory_path']
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// Configuration constants
|
|
4
|
+
const DEFAULT_READ_LIMIT = 2000;
|
|
5
|
+
const MAX_LINE_LENGTH = 2000;
|
|
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 readFile(filePath, offset, limit) {
|
|
50
|
+
try {
|
|
51
|
+
const validPath = await validatePath(filePath);
|
|
52
|
+
// Check if file exists and is actually a file
|
|
53
|
+
const stats = await fs.stat(validPath);
|
|
54
|
+
if (!stats.isFile()) {
|
|
55
|
+
throw new Error('Path is not a file');
|
|
56
|
+
}
|
|
57
|
+
// Read and return file content
|
|
58
|
+
const content = await fs.readFile(validPath, 'utf-8');
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
// Apply offset and limit if specified
|
|
61
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0; // Convert to 0-based indexing
|
|
62
|
+
const endLine = limit ? Math.min(lines.length, startLine + limit) : Math.min(lines.length, startLine + DEFAULT_READ_LIMIT);
|
|
63
|
+
// Truncate lines that are too long
|
|
64
|
+
const processedLines = lines.slice(startLine, endLine).map(line => line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + '...[truncated]' : line);
|
|
65
|
+
const result = processedLines.join('\n');
|
|
66
|
+
// Add metadata if file was truncated or offset was used
|
|
67
|
+
let metadata = '';
|
|
68
|
+
if (offset && offset > 1) {
|
|
69
|
+
metadata += `\n[File started at line ${offset}]`;
|
|
70
|
+
}
|
|
71
|
+
if (endLine < lines.length) {
|
|
72
|
+
metadata += `\n[File truncated - showing lines ${startLine + 1}-${endLine} of ${lines.length} total lines]`;
|
|
73
|
+
}
|
|
74
|
+
if (processedLines.some(line => line.includes('...[truncated]'))) {
|
|
75
|
+
metadata += `\n[Some lines truncated at ${MAX_LINE_LENGTH} characters]`;
|
|
76
|
+
}
|
|
77
|
+
return result + metadata;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
throw new Error('Failed to read file: Unknown error');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Tool definition
|
|
87
|
+
export const readFileTool = {
|
|
88
|
+
type: 'function',
|
|
89
|
+
function: {
|
|
90
|
+
name: 'read_file',
|
|
91
|
+
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. Supports reading specific line ranges for large files.',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
file_path: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'The path to the file to read, relative to the current working directory. Examples: "src/index.ts", "package.json", "README.md"'
|
|
98
|
+
},
|
|
99
|
+
offset: {
|
|
100
|
+
type: 'integer',
|
|
101
|
+
description: 'Optional: The 1-based line number to start reading from. If not specified, reads from the beginning of the file.'
|
|
102
|
+
},
|
|
103
|
+
limit: {
|
|
104
|
+
type: 'integer',
|
|
105
|
+
description: 'Optional: The maximum number of lines to read. If not specified, reads up to 2000 lines. Use this with offset to read specific sections of large files.'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
required: ['file_path']
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
// Current working directory for file operations
|
|
4
|
+
const workingDirectory = process.cwd();
|
|
5
|
+
// Global flags and session state
|
|
6
|
+
let globalConfig = null;
|
|
7
|
+
let dangerouslyAcceptAll = false;
|
|
8
|
+
let approvedCommandsForSession = new Set();
|
|
9
|
+
// Whitelisted safe commands that don't require confirmation
|
|
10
|
+
const SAFE_COMMANDS = [
|
|
11
|
+
'ls', 'dir', 'pwd', 'whoami', 'date', 'echo', 'cat', 'head', 'tail',
|
|
12
|
+
'grep', 'find', 'wc', 'sort', 'uniq', 'cut', 'awk', 'sed',
|
|
13
|
+
'git status', 'git log', 'git diff', 'git branch', 'git show',
|
|
14
|
+
'npm list', 'npm ls', 'yarn list', 'node --version', 'npm --version',
|
|
15
|
+
'python --version', 'python3 --version', 'which', 'type', 'file'
|
|
16
|
+
];
|
|
17
|
+
export function setShellConfig(config) {
|
|
18
|
+
globalConfig = config;
|
|
19
|
+
}
|
|
20
|
+
export function setDangerouslyAcceptAll(accept) {
|
|
21
|
+
dangerouslyAcceptAll = accept;
|
|
22
|
+
}
|
|
23
|
+
export async function runShellCommand(command, args = [], timeoutMs = 30000) {
|
|
24
|
+
return await runShellCommandWithRetry(command, args, timeoutMs, 0);
|
|
25
|
+
}
|
|
26
|
+
async function runShellCommandWithRetry(command, args = [], timeoutMs = 30000, retryCount = 0) {
|
|
27
|
+
const maxRetries = 2;
|
|
28
|
+
try {
|
|
29
|
+
// Security: Basic validation to prevent obviously dangerous commands
|
|
30
|
+
const dangerousCommands = ['rm -rf', 'sudo', 'su', 'chmod 777', 'dd if=', 'mkfs', 'fdisk', 'format'];
|
|
31
|
+
const fullCommand = `${command} ${args.join(' ')}`.toLowerCase();
|
|
32
|
+
for (const dangerous of dangerousCommands) {
|
|
33
|
+
if (fullCommand.includes(dangerous)) {
|
|
34
|
+
throw new Error(`Command contains potentially dangerous operation: ${dangerous}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const commandString = `${command} ${args.join(' ')}`;
|
|
38
|
+
const baseCommand = command.toLowerCase();
|
|
39
|
+
// Check if we should auto-approve (only show approval prompt on first attempt)
|
|
40
|
+
if (retryCount === 0) {
|
|
41
|
+
if (dangerouslyAcceptAll) {
|
|
42
|
+
console.log(`🚀 Auto-executing (--dangerously-accept-all): ${commandString}`);
|
|
43
|
+
}
|
|
44
|
+
else if (approvedCommandsForSession.has(baseCommand)) {
|
|
45
|
+
console.log(`🚀 Auto-executing (${baseCommand} approved for session): ${commandString}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Check if it's a safe command
|
|
49
|
+
const isSafeCommand = SAFE_COMMANDS.some(safe => {
|
|
50
|
+
const normalizedSafe = safe.toLowerCase();
|
|
51
|
+
const normalizedFull = fullCommand.toLowerCase();
|
|
52
|
+
return normalizedFull === normalizedSafe ||
|
|
53
|
+
(normalizedSafe.includes(' ') && normalizedFull.startsWith(normalizedSafe)) ||
|
|
54
|
+
(!normalizedSafe.includes(' ') && normalizedFull.split(' ')[0] === normalizedSafe);
|
|
55
|
+
});
|
|
56
|
+
if (isSafeCommand) {
|
|
57
|
+
console.log(`🟢 Executing safe command: ${commandString}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Require user confirmation with enhanced options
|
|
61
|
+
console.log(`\n🔐 Shell Command Requested: ${commandString}`);
|
|
62
|
+
console.log(`📁 Working Directory: ${workingDirectory}`);
|
|
63
|
+
const { choice } = await inquirer.prompt([
|
|
64
|
+
{
|
|
65
|
+
type: 'list',
|
|
66
|
+
name: 'choice',
|
|
67
|
+
message: 'Choose your action:',
|
|
68
|
+
choices: [
|
|
69
|
+
{ name: '1. ✅ Execute this command now', value: 'execute' },
|
|
70
|
+
{ name: `2. ✅ Execute and approve all "${baseCommand}" commands for this session`, value: 'approve_session' },
|
|
71
|
+
{ name: '3. ❌ Cancel and suggest alternative', value: 'cancel' }
|
|
72
|
+
],
|
|
73
|
+
default: 'execute'
|
|
74
|
+
}
|
|
75
|
+
]);
|
|
76
|
+
switch (choice) {
|
|
77
|
+
case 'execute':
|
|
78
|
+
console.log(`🚀 Executing: ${commandString}`);
|
|
79
|
+
break;
|
|
80
|
+
case 'approve_session':
|
|
81
|
+
approvedCommandsForSession.add(baseCommand);
|
|
82
|
+
console.log(`🔓 "${baseCommand}" approved for session - all future ${baseCommand} commands will auto-execute`);
|
|
83
|
+
console.log(`🚀 Executing: ${commandString}`);
|
|
84
|
+
break;
|
|
85
|
+
case 'cancel':
|
|
86
|
+
return getSuggestion(commandString);
|
|
87
|
+
default:
|
|
88
|
+
return `Command execution cancelled by user: ${commandString}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`🔄 Retry attempt ${retryCount}/${maxRetries}: ${commandString}`);
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const child = spawn(command, args, {
|
|
98
|
+
cwd: workingDirectory,
|
|
99
|
+
stdio: ['pipe', 'pipe', 'pipe'], // Capture all input/output for AI processing
|
|
100
|
+
shell: true,
|
|
101
|
+
env: { ...process.env, PATH: process.env.PATH }
|
|
102
|
+
});
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
let completed = false;
|
|
106
|
+
// Set up timeout
|
|
107
|
+
const timeout = setTimeout(() => {
|
|
108
|
+
if (!completed) {
|
|
109
|
+
completed = true;
|
|
110
|
+
child.kill('SIGTERM');
|
|
111
|
+
reject(new Error(`TIMEOUT_ERROR:${timeoutMs}`));
|
|
112
|
+
}
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
child.stdout?.on('data', (data) => {
|
|
115
|
+
stdout += data.toString();
|
|
116
|
+
});
|
|
117
|
+
child.stderr?.on('data', (data) => {
|
|
118
|
+
stderr += data.toString();
|
|
119
|
+
});
|
|
120
|
+
child.on('close', (code) => {
|
|
121
|
+
if (!completed) {
|
|
122
|
+
completed = true;
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
const output = stdout + (stderr ? `\nSTDERR:\n${stderr}` : '');
|
|
125
|
+
if (code === 0) {
|
|
126
|
+
resolve(`Command executed successfully (exit code: ${code})\n\nOutput:\n${output || '(no output)'}`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// For non-zero exit codes, still return the output but indicate the failure
|
|
130
|
+
resolve(`Command exited with code ${code}\n\nOutput:\n${output || '(no output)'}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
child.on('error', (error) => {
|
|
135
|
+
if (!completed) {
|
|
136
|
+
completed = true;
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
reject(new Error(`Failed to execute command: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
// Check if this is a timeout error and we haven't exceeded max retries
|
|
146
|
+
if (error.message.startsWith('TIMEOUT_ERROR:') && retryCount < maxRetries) {
|
|
147
|
+
const timeoutDuration = parseInt(error.message.split(':')[1]);
|
|
148
|
+
return await analyzeTimeoutAndRetry(command, args, timeoutDuration, retryCount);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Shell command error: ${error.message.replace('TIMEOUT_ERROR:', 'Command timed out after ').replace(/:\d+/, 'ms')}`);
|
|
151
|
+
}
|
|
152
|
+
throw new Error('Shell command error: Unknown error');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function analyzeTimeoutAndRetry(command, args = [], originalTimeout, retryCount) {
|
|
156
|
+
const commandString = `${command} ${args.join(' ')}`;
|
|
157
|
+
console.log(`⏱️ Command timed out after ${originalTimeout}ms: ${commandString}`);
|
|
158
|
+
console.log(`🔍 Analyzing timeout cause and preparing retry...`);
|
|
159
|
+
// Analyze the command to understand why it might have timed out
|
|
160
|
+
const analysis = analyzeCommandForTimeout(command, args);
|
|
161
|
+
console.log(`💡 Timeout analysis: ${analysis.reason}`);
|
|
162
|
+
if (analysis.suggestedArgs.length > 0) {
|
|
163
|
+
console.log(`🔧 Suggested fix: ${command} ${analysis.suggestedArgs.join(' ')}`);
|
|
164
|
+
// Try with suggested arguments
|
|
165
|
+
return await runShellCommandWithRetry(command, analysis.suggestedArgs, analysis.suggestedTimeout, retryCount + 1);
|
|
166
|
+
}
|
|
167
|
+
else if (analysis.suggestedTimeout > originalTimeout) {
|
|
168
|
+
console.log(`⏰ Increasing timeout to ${analysis.suggestedTimeout}ms for long-running operation`);
|
|
169
|
+
// Try with longer timeout
|
|
170
|
+
return await runShellCommandWithRetry(command, args, analysis.suggestedTimeout, retryCount + 1);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// No specific fix found, provide analysis
|
|
174
|
+
return `Command timed out after ${originalTimeout}ms: ${commandString}\n\nTimeout Analysis: ${analysis.reason}\n\nSuggestion: ${analysis.suggestion}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function analyzeCommandForTimeout(command, args) {
|
|
178
|
+
const fullCommand = `${command} ${args.join(' ')}`.toLowerCase();
|
|
179
|
+
const baseCommand = command.toLowerCase();
|
|
180
|
+
// Interactive command detection
|
|
181
|
+
if (baseCommand === 'npm' && args.length > 0) {
|
|
182
|
+
const npmSubcommand = args[0].toLowerCase();
|
|
183
|
+
if (npmSubcommand === 'create' || npmSubcommand === 'init') {
|
|
184
|
+
// Check if template is specified for npm create
|
|
185
|
+
if (npmSubcommand === 'create' && !args.some(arg => arg.includes('--template'))) {
|
|
186
|
+
return {
|
|
187
|
+
reason: "npm create command likely waiting for interactive template selection",
|
|
188
|
+
suggestion: "Add --template flag to avoid interactive prompts",
|
|
189
|
+
suggestedArgs: [...args, '--template', 'vanilla'],
|
|
190
|
+
suggestedTimeout: 60000
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// Check for other interactive npm commands
|
|
194
|
+
if (!args.some(arg => arg.includes('-y') || arg.includes('--yes'))) {
|
|
195
|
+
return {
|
|
196
|
+
reason: "npm command likely waiting for interactive confirmation",
|
|
197
|
+
suggestion: "Add -y flag to auto-confirm prompts",
|
|
198
|
+
suggestedArgs: [...args, '-y'],
|
|
199
|
+
suggestedTimeout: 60000
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (npmSubcommand === 'install' || npmSubcommand === 'i') {
|
|
204
|
+
return {
|
|
205
|
+
reason: "npm install operations can take a long time",
|
|
206
|
+
suggestion: "Increase timeout for package installation",
|
|
207
|
+
suggestedArgs: args,
|
|
208
|
+
suggestedTimeout: 120000 // 2 minutes
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Git operations
|
|
213
|
+
if (baseCommand === 'git') {
|
|
214
|
+
const gitSubcommand = args[0]?.toLowerCase();
|
|
215
|
+
if (['clone', 'pull', 'push', 'fetch'].includes(gitSubcommand)) {
|
|
216
|
+
return {
|
|
217
|
+
reason: "Git network operations can be slow",
|
|
218
|
+
suggestion: "Increase timeout for network operations",
|
|
219
|
+
suggestedArgs: args,
|
|
220
|
+
suggestedTimeout: 90000 // 1.5 minutes
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (gitSubcommand === 'commit' && !args.some(arg => arg.includes('-m'))) {
|
|
224
|
+
return {
|
|
225
|
+
reason: "Git commit without -m flag opens interactive editor",
|
|
226
|
+
suggestion: "Add commit message with -m flag",
|
|
227
|
+
suggestedArgs: [...args, '-m', '"Auto-commit"'],
|
|
228
|
+
suggestedTimeout: 30000
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Yarn operations
|
|
233
|
+
if (baseCommand === 'yarn') {
|
|
234
|
+
if (args.length === 0 || args[0] === 'install') {
|
|
235
|
+
return {
|
|
236
|
+
reason: "Yarn install operations can take a long time",
|
|
237
|
+
suggestion: "Increase timeout for package installation",
|
|
238
|
+
suggestedArgs: args,
|
|
239
|
+
suggestedTimeout: 120000 // 2 minutes
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (args[0] === 'create' && !args.some(arg => arg.includes('--template'))) {
|
|
243
|
+
return {
|
|
244
|
+
reason: "yarn create command likely waiting for interactive input",
|
|
245
|
+
suggestion: "Add template specification to avoid prompts",
|
|
246
|
+
suggestedArgs: [...args, '--template', 'default'],
|
|
247
|
+
suggestedTimeout: 60000
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Build operations
|
|
252
|
+
if (['build', 'compile', 'webpack', 'rollup', 'vite'].includes(baseCommand)) {
|
|
253
|
+
return {
|
|
254
|
+
reason: "Build operations often require more time",
|
|
255
|
+
suggestion: "Increase timeout for build processes",
|
|
256
|
+
suggestedArgs: args,
|
|
257
|
+
suggestedTimeout: 120000 // 2 minutes
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// Testing operations
|
|
261
|
+
if (['test', 'jest', 'mocha', 'karma'].includes(baseCommand) ||
|
|
262
|
+
(baseCommand === 'npm' && args[0] === 'test')) {
|
|
263
|
+
return {
|
|
264
|
+
reason: "Test operations can take significant time",
|
|
265
|
+
suggestion: "Increase timeout for test execution",
|
|
266
|
+
suggestedArgs: args,
|
|
267
|
+
suggestedTimeout: 90000 // 1.5 minutes
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// Python operations
|
|
271
|
+
if (['python', 'python3', 'pip', 'pip3'].includes(baseCommand)) {
|
|
272
|
+
if (args.some(arg => arg.includes('install'))) {
|
|
273
|
+
return {
|
|
274
|
+
reason: "Python package installation can be slow",
|
|
275
|
+
suggestion: "Increase timeout for package installation",
|
|
276
|
+
suggestedArgs: args,
|
|
277
|
+
suggestedTimeout: 120000 // 2 minutes
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Default analysis for unknown timeouts
|
|
282
|
+
return {
|
|
283
|
+
reason: "Command exceeded default timeout, possibly due to network delays, large operations, or interactive prompts",
|
|
284
|
+
suggestion: "Try adding flags to make the command non-interactive (like -y, --yes, --no-interactive) or check if the command is waiting for user input",
|
|
285
|
+
suggestedArgs: [],
|
|
286
|
+
suggestedTimeout: 60000 // 1 minute default increase
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Helper function to provide suggestions for cancelled commands
|
|
290
|
+
function getSuggestion(commandString) {
|
|
291
|
+
const suggestions = {
|
|
292
|
+
'rm': 'Consider using file tools like write_file or edit_file to manage files safely',
|
|
293
|
+
'sudo': 'ProtoAgent runs with your user permissions only for security',
|
|
294
|
+
'npm install': 'Try: Use the file tools to examine package.json first, then I can suggest safer alternatives',
|
|
295
|
+
'npm create': 'Add flags to avoid interactive prompts: npm create vite@latest my-app --template react',
|
|
296
|
+
'git push': 'Consider: First check git status, then review changes before pushing',
|
|
297
|
+
'chmod': 'Consider: Use file tools to check permissions first, specific chmod may not be needed'
|
|
298
|
+
};
|
|
299
|
+
const cmd = commandString.split(' ')[0].toLowerCase();
|
|
300
|
+
const fullCmd = commandString.toLowerCase();
|
|
301
|
+
// Check for interactive command patterns
|
|
302
|
+
if (fullCmd.includes('npm create') && !fullCmd.includes('--template')) {
|
|
303
|
+
return `Command cancelled: ${commandString}\n\n💡 Suggestion: npm create commands require template specification to avoid interactive prompts. Try: npm create vite@latest my-app --template react\n\nAvailable templates: vanilla, vue, react, preact, lit, svelte, solid, qwik, angular`;
|
|
304
|
+
}
|
|
305
|
+
if (fullCmd.includes('git commit') && !fullCmd.includes('-m')) {
|
|
306
|
+
return `Command cancelled: ${commandString}\n\n💡 Suggestion: git commit requires a message flag to avoid opening an interactive editor. Try: git commit -m "Your commit message"`;
|
|
307
|
+
}
|
|
308
|
+
const suggestion = suggestions[cmd] || suggestions[fullCmd.split(' ').slice(0, 2).join(' ')] ||
|
|
309
|
+
'Consider using the available file system tools (read_file, write_file, list_directory) for safer operations, or add flags to make commands non-interactive';
|
|
310
|
+
return `Command cancelled: ${commandString}\n\n💡 Suggestion: ${suggestion}\n\nYou can:\n- Use 'protoagent --dangerously-accept-all' to auto-approve all commands\n- Choose option 2 next time to approve commands for the session\n- Add flags to make commands non-interactive (e.g., --template, --yes, --no-interactive, -m for git)\n- ProtoAgent can automatically retry timed-out commands with better parameters\n- Ask me to break down the task into safer operations`;
|
|
311
|
+
}
|
|
312
|
+
// Tool definition
|
|
313
|
+
export const runShellCommandTool = {
|
|
314
|
+
type: 'function',
|
|
315
|
+
function: {
|
|
316
|
+
name: 'run_shell_command',
|
|
317
|
+
description: 'Execute a shell command in the current working directory. Commands run non-interactively and output is captured for analysis. For tools that normally prompt for input (like npm create), provide all necessary flags to avoid interactive prompts. Safe commands (ls, find, grep, git status, etc.) run automatically. Other commands may require user confirmation unless running with --dangerously-accept-all flag. Examples: find . -name "*.js", grep -r "TODO" ., npm create vite@latest my-app --template react --no-interactive',
|
|
318
|
+
parameters: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: {
|
|
321
|
+
command: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'The command to execute. Examples: "find", "grep", "ls", "git", "npm", "python", "node", "yarn"'
|
|
324
|
+
},
|
|
325
|
+
args: {
|
|
326
|
+
type: 'array',
|
|
327
|
+
items: {
|
|
328
|
+
type: 'string'
|
|
329
|
+
},
|
|
330
|
+
description: 'Arguments to pass to the command. Examples: [".", "-name", "*.js"] for find, ["-r", "TODO", "."] for grep, ["-la"] for ls'
|
|
331
|
+
},
|
|
332
|
+
timeout_ms: {
|
|
333
|
+
type: 'integer',
|
|
334
|
+
description: 'Timeout in milliseconds for the command execution. Default is 30000 (30 seconds). Use higher values for long-running operations.'
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
required: ['command']
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|