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,136 @@
|
|
|
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 writeFile(filePath, content) {
|
|
50
|
+
try {
|
|
51
|
+
const validPath = await validatePath(filePath);
|
|
52
|
+
// Check if file already exists for approval context
|
|
53
|
+
let fileExists = false;
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(validPath);
|
|
56
|
+
fileExists = true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// File doesn't exist, which is expected for write operations
|
|
60
|
+
}
|
|
61
|
+
// Request user approval for write operation
|
|
62
|
+
const approvalContext = {
|
|
63
|
+
operation: 'write',
|
|
64
|
+
filePath: filePath,
|
|
65
|
+
description: fileExists
|
|
66
|
+
? `Overwrite existing file with ${content.length} characters of new content`
|
|
67
|
+
: `Create new file with ${content.length} characters of content`,
|
|
68
|
+
contentPreview: content.length > 500
|
|
69
|
+
? `${content.substring(0, 250)}...\n\n...${content.substring(content.length - 250)}`
|
|
70
|
+
: content
|
|
71
|
+
};
|
|
72
|
+
const approved = await requestFileOperationApproval(approvalContext);
|
|
73
|
+
if (!approved) {
|
|
74
|
+
return `Write operation cancelled by user: ${filePath}`;
|
|
75
|
+
}
|
|
76
|
+
logger.debug(`š Writing file: ${filePath} (${content.length} chars)`, { component: 'WriteFile', operation: 'writeFile' });
|
|
77
|
+
try {
|
|
78
|
+
// Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
|
|
79
|
+
// preventing writes through pre-existing symlinks
|
|
80
|
+
await fs.writeFile(validPath, content, { encoding: "utf-8", flag: 'wx' });
|
|
81
|
+
logger.info(`ā
Created new file: ${filePath}`, { component: 'WriteFile', operation: 'writeFile' });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === 'EEXIST') {
|
|
85
|
+
// Security: Use atomic rename to prevent race conditions where symlinks
|
|
86
|
+
// could be created between validation and write. Rename operations
|
|
87
|
+
// replace the target file atomically and don't follow symlinks.
|
|
88
|
+
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
|
|
89
|
+
try {
|
|
90
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
91
|
+
await fs.rename(tempPath, validPath);
|
|
92
|
+
logger.info(`ā
Overwrote existing file: ${filePath}`, { component: 'WriteFile', operation: 'writeFile' });
|
|
93
|
+
}
|
|
94
|
+
catch (renameError) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.unlink(tempPath);
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
throw renameError;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return `Successfully wrote to ${filePath}`;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof Error) {
|
|
110
|
+
throw new Error(`Failed to write file: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
throw new Error('Failed to write file: Unknown error');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Tool definition
|
|
116
|
+
export const writeFileTool = {
|
|
117
|
+
type: 'function',
|
|
118
|
+
function: {
|
|
119
|
+
name: 'write_file',
|
|
120
|
+
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.',
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
file_path: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'The path to the file to write, relative to the current working directory. Examples: "src/newfile.ts", "config.json", "README.md"'
|
|
127
|
+
},
|
|
128
|
+
content: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'The content to write to the file. This will completely replace any existing content.'
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
required: ['file_path', 'content']
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation history compaction utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Provides the system prompt for the history compression process.
|
|
6
|
+
* This prompt instructs the model to act as a specialized state manager,
|
|
7
|
+
* think in a scratchpad, and produce a structured XML summary.
|
|
8
|
+
*/
|
|
9
|
+
export function getCompressionPrompt() {
|
|
10
|
+
return `
|
|
11
|
+
You are the component that summarizes internal chat history into a given structure.
|
|
12
|
+
|
|
13
|
+
When the conversation history grows too large, you will be invoked to distill the entire history into a concise, structured XML snapshot. This snapshot is CRITICAL, as it will become the agent's *only* memory of the past. The agent will resume its work based solely on this snapshot. All crucial details, plans, errors, and user directives MUST be preserved.
|
|
14
|
+
|
|
15
|
+
First, you will think through the entire history in a private <scratchpad>. Review the user's overall goal, the agent's actions, tool outputs, file modifications, and any unresolved questions. Identify every piece of information that is essential for future actions.
|
|
16
|
+
|
|
17
|
+
After your reasoning is complete, generate the final <state_snapshot> XML object. Be incredibly dense with information. Omit any irrelevant conversational filler.
|
|
18
|
+
|
|
19
|
+
The structure MUST be as follows:
|
|
20
|
+
|
|
21
|
+
<state_snapshot>
|
|
22
|
+
<overall_goal>
|
|
23
|
+
<!-- A single, concise sentence describing the user's high-level objective. -->
|
|
24
|
+
<!-- Example: "Refactor the authentication service to use a new JWT library." -->
|
|
25
|
+
</overall_goal>
|
|
26
|
+
|
|
27
|
+
<key_knowledge>
|
|
28
|
+
<!-- Crucial facts, conventions, and constraints the agent must remember based on the conversation history and interaction with the user. Use bullet points. -->
|
|
29
|
+
<!-- Example:
|
|
30
|
+
- Build Command: \`npm run build\`
|
|
31
|
+
- Testing: Tests are run with \`npm test\`. Test files must end in \`.test.ts\`.
|
|
32
|
+
- API Endpoint: The primary API endpoint is \`https://api.example.com/v2\`.
|
|
33
|
+
|
|
34
|
+
-->
|
|
35
|
+
</key_knowledge>
|
|
36
|
+
|
|
37
|
+
<file_system_state>
|
|
38
|
+
<!-- List files that have been created, read, modified, or deleted. Note their status and critical learnings. -->
|
|
39
|
+
<!-- Example:
|
|
40
|
+
- CWD: \`/home/user/project/src\`
|
|
41
|
+
- READ: \`package.json\` - Confirmed 'axios' is a dependency.
|
|
42
|
+
- MODIFIED: \`services/auth.ts\` - Replaced 'jsonwebtoken' with 'jose'.
|
|
43
|
+
- CREATED: \`tests/new-feature.test.ts\` - Initial test structure for the new feature.
|
|
44
|
+
-->
|
|
45
|
+
</file_system_state>
|
|
46
|
+
|
|
47
|
+
<recent_actions>
|
|
48
|
+
<!-- A summary of the last few significant agent actions and their outcomes. Focus on facts. -->
|
|
49
|
+
<!-- Example:
|
|
50
|
+
- Ran \`grep 'old_function'\` which returned 3 results in 2 files.
|
|
51
|
+
- Ran \`npm run test\`, which failed due to a snapshot mismatch in \`UserProfile.test.ts\`.
|
|
52
|
+
- Ran \`ls -F static/\` and discovered image assets are stored as \`.webp\`.
|
|
53
|
+
-->
|
|
54
|
+
</recent_actions>
|
|
55
|
+
|
|
56
|
+
<current_plan>
|
|
57
|
+
<!-- The agent's step-by-step plan. Mark completed steps. -->
|
|
58
|
+
<!-- Example:
|
|
59
|
+
1. [DONE] Identify all files using the deprecated 'UserAPI'.
|
|
60
|
+
2. [IN PROGRESS] Refactor \`src/components/UserProfile.tsx\` to use the new 'ProfileAPI'.
|
|
61
|
+
3. [TODO] Refactor the remaining files.
|
|
62
|
+
4. [TODO] Update tests to reflect the API change.
|
|
63
|
+
-->
|
|
64
|
+
</current_plan>
|
|
65
|
+
</state_snapshot>
|
|
66
|
+
`.trim();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compact conversation history by creating a summary
|
|
70
|
+
*/
|
|
71
|
+
export async function compactConversation(client, model, messages, recentMessagesToKeep = 5) {
|
|
72
|
+
console.log('\nšļø Compacting conversation history to manage context window...');
|
|
73
|
+
// Keep the system message and recent messages
|
|
74
|
+
const systemMessage = messages.find(m => m.role === 'system');
|
|
75
|
+
const recentMessages = messages.slice(-recentMessagesToKeep);
|
|
76
|
+
// Get the history to compress (excluding system and recent messages)
|
|
77
|
+
const historyToCompress = messages.slice(systemMessage ? 1 : 0, -recentMessagesToKeep);
|
|
78
|
+
if (historyToCompress.length === 0) {
|
|
79
|
+
console.log(' No history to compress, keeping current messages');
|
|
80
|
+
return messages;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
// Create compression request
|
|
84
|
+
const compressionMessages = [
|
|
85
|
+
{
|
|
86
|
+
role: 'system',
|
|
87
|
+
content: getCompressionPrompt()
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
role: 'user',
|
|
91
|
+
content: `Please compress the following conversation history:\n\n${JSON.stringify(historyToCompress, null, 2)}`
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
// Get compression summary (non-streaming for simplicity)
|
|
95
|
+
const response = await client.chat.completions.create({
|
|
96
|
+
model,
|
|
97
|
+
messages: compressionMessages,
|
|
98
|
+
max_tokens: 2000,
|
|
99
|
+
temperature: 0.1
|
|
100
|
+
});
|
|
101
|
+
const compressionSummary = response.choices[0]?.message?.content;
|
|
102
|
+
if (!compressionSummary) {
|
|
103
|
+
console.log(' Failed to generate compression summary, keeping original messages');
|
|
104
|
+
return messages;
|
|
105
|
+
}
|
|
106
|
+
// Create the compressed history message
|
|
107
|
+
const compressedMessage = {
|
|
108
|
+
role: 'system',
|
|
109
|
+
content: `Previous conversation summary:\n\n${compressionSummary}`
|
|
110
|
+
};
|
|
111
|
+
// Build new message array
|
|
112
|
+
const compactedMessages = [];
|
|
113
|
+
// Add original system message if it exists
|
|
114
|
+
if (systemMessage) {
|
|
115
|
+
compactedMessages.push(systemMessage);
|
|
116
|
+
}
|
|
117
|
+
// Add the compressed history
|
|
118
|
+
compactedMessages.push(compressedMessage);
|
|
119
|
+
// Add recent messages
|
|
120
|
+
compactedMessages.push(...recentMessages);
|
|
121
|
+
console.log(` ā
Compressed ${historyToCompress.length} messages into summary`);
|
|
122
|
+
console.log(` š Keeping ${recentMessages.length} recent messages`);
|
|
123
|
+
return compactedMessages;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.log(` ā Compression failed: ${error}. Keeping original messages.`);
|
|
127
|
+
return messages;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if conversation needs compaction and perform it if necessary
|
|
132
|
+
*/
|
|
133
|
+
export async function checkAndCompactIfNeeded(client, model, messages, maxTokens, currentTokens) {
|
|
134
|
+
const utilizationPercentage = (currentTokens / maxTokens) * 100;
|
|
135
|
+
if (utilizationPercentage >= 90) {
|
|
136
|
+
return await compactConversation(client, model, messages);
|
|
137
|
+
}
|
|
138
|
+
return messages;
|
|
139
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost tracking and token counting utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Rough token estimation for OpenAI models
|
|
6
|
+
* This is approximate - actual tokenization may vary
|
|
7
|
+
*/
|
|
8
|
+
export function estimateTokens(text) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return 0;
|
|
11
|
+
// Rough estimation: ~4 characters per token for English text
|
|
12
|
+
// This is conservative and should be close enough for cost estimation
|
|
13
|
+
return Math.ceil(text.length / 4);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Estimate tokens for a message
|
|
17
|
+
*/
|
|
18
|
+
export function estimateMessageTokens(message) {
|
|
19
|
+
let tokens = 0;
|
|
20
|
+
// Base overhead per message
|
|
21
|
+
tokens += 4;
|
|
22
|
+
if ('content' in message && message.content) {
|
|
23
|
+
if (typeof message.content === 'string') {
|
|
24
|
+
tokens += estimateTokens(message.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if ('tool_calls' in message && message.tool_calls) {
|
|
28
|
+
for (const toolCall of message.tool_calls) {
|
|
29
|
+
tokens += estimateTokens(toolCall.function.name);
|
|
30
|
+
tokens += estimateTokens(toolCall.function.arguments);
|
|
31
|
+
tokens += 10; // overhead per tool call
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return tokens;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Estimate total tokens for conversation history
|
|
38
|
+
*/
|
|
39
|
+
export function estimateConversationTokens(messages) {
|
|
40
|
+
let totalTokens = 0;
|
|
41
|
+
for (const message of messages) {
|
|
42
|
+
totalTokens += estimateMessageTokens(message);
|
|
43
|
+
}
|
|
44
|
+
// Add some overhead for the conversation structure
|
|
45
|
+
totalTokens += 10;
|
|
46
|
+
return totalTokens;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Calculate estimated cost for a request
|
|
50
|
+
*/
|
|
51
|
+
export function calculateCost(inputTokens, outputTokens, modelConfig) {
|
|
52
|
+
const inputCost = inputTokens * modelConfig.pricing.inputTokens;
|
|
53
|
+
const outputCost = outputTokens * modelConfig.pricing.outputTokens;
|
|
54
|
+
return inputCost + outputCost;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get context information for the current conversation
|
|
58
|
+
*/
|
|
59
|
+
export function getContextInfo(messages, modelConfig) {
|
|
60
|
+
const currentTokens = estimateConversationTokens(messages);
|
|
61
|
+
const maxTokens = modelConfig.contextWindow;
|
|
62
|
+
const utilizationPercentage = (currentTokens / maxTokens) * 100;
|
|
63
|
+
const needsCompaction = utilizationPercentage >= 90; // Compact at 90% capacity
|
|
64
|
+
return {
|
|
65
|
+
currentTokens,
|
|
66
|
+
maxTokens,
|
|
67
|
+
utilizationPercentage,
|
|
68
|
+
needsCompaction
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Log usage and cost information
|
|
73
|
+
*/
|
|
74
|
+
export function logUsageInfo(usage, contextInfo, modelConfig) {
|
|
75
|
+
console.log(`š° Usage: ${usage.inputTokens} in + ${usage.outputTokens} out = ${usage.totalTokens} tokens`);
|
|
76
|
+
console.log(`šø Estimated cost: $${usage.estimatedCost.toFixed(6)}`);
|
|
77
|
+
console.log(`š Context: ${contextInfo.currentTokens}/${contextInfo.maxTokens} tokens (${contextInfo.utilizationPercentage.toFixed(1)}%)`);
|
|
78
|
+
if (contextInfo.needsCompaction) {
|
|
79
|
+
console.log(`ā ļø Context approaching limit - automatic compaction will trigger soon`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create usage info from response tokens
|
|
84
|
+
*/
|
|
85
|
+
export function createUsageInfo(inputTokens, outputTokens, modelConfig) {
|
|
86
|
+
const totalTokens = inputTokens + outputTokens;
|
|
87
|
+
const estimatedCost = calculateCost(inputTokens, outputTokens, modelConfig);
|
|
88
|
+
return {
|
|
89
|
+
inputTokens,
|
|
90
|
+
outputTokens,
|
|
91
|
+
totalTokens,
|
|
92
|
+
estimatedCost
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Extract token usage from OpenAI response
|
|
97
|
+
*/
|
|
98
|
+
export function extractTokenUsage(response) {
|
|
99
|
+
// For streaming responses, we need to estimate
|
|
100
|
+
// In a real implementation, you might want to sum up the tokens from the stream
|
|
101
|
+
// For now, we'll use rough estimation
|
|
102
|
+
return {
|
|
103
|
+
inputTokens: 0, // Will be estimated separately
|
|
104
|
+
outputTokens: 0 // Will be estimated separately
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File operations approval utility for ProtoAgent
|
|
3
|
+
* Handles user acceptance for write and edit file operations
|
|
4
|
+
*/
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
// Global state for approval settings
|
|
8
|
+
let globalConfig = null;
|
|
9
|
+
let dangerouslyAcceptAll = false;
|
|
10
|
+
let approvedOperationsForSession = new Set();
|
|
11
|
+
export function setFileOperationConfig(config) {
|
|
12
|
+
globalConfig = config;
|
|
13
|
+
}
|
|
14
|
+
export function setDangerouslyAcceptAllFileOps(accept) {
|
|
15
|
+
dangerouslyAcceptAll = accept;
|
|
16
|
+
if (accept) {
|
|
17
|
+
logger.warn('ā ļø DANGER MODE: All file operations will be auto-approved without confirmation!');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Request user approval for file write/edit operations
|
|
22
|
+
*/
|
|
23
|
+
export async function requestFileOperationApproval(context) {
|
|
24
|
+
const { operation, filePath, description, contentPreview } = context;
|
|
25
|
+
// Check if we should auto-approve
|
|
26
|
+
if (dangerouslyAcceptAll) {
|
|
27
|
+
logger.info(`š Auto-approving (--dangerously-accept-all): ${operation} ${filePath}`);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const operationType = operation === 'write' ? 'write_file' : 'edit_file';
|
|
31
|
+
if (approvedOperationsForSession.has(operationType)) {
|
|
32
|
+
logger.info(`š Auto-approving (${operationType} approved for session): ${operation} ${filePath}`);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Show detailed information about the operation
|
|
36
|
+
console.log(`\nš File Operation Requested: ${operation.toUpperCase()}`);
|
|
37
|
+
console.log(`š File: ${filePath}`);
|
|
38
|
+
console.log(`š Description: ${description}`);
|
|
39
|
+
if (contentPreview) {
|
|
40
|
+
console.log(`\nš Content Preview:`);
|
|
41
|
+
const lines = contentPreview.split('\n');
|
|
42
|
+
if (lines.length > 10) {
|
|
43
|
+
console.log(lines.slice(0, 5).join('\n'));
|
|
44
|
+
console.log(`... (${lines.length - 10} more lines) ...`);
|
|
45
|
+
console.log(lines.slice(-5).join('\n'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(contentPreview);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Ask for user approval with enhanced options
|
|
52
|
+
const { choice } = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'choice',
|
|
56
|
+
message: 'Choose your action:',
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: `1. ā
Approve this ${operation} operation`, value: 'approve' },
|
|
59
|
+
{ name: `2. ā
Approve and allow all ${operation} operations for this session`, value: 'approve_session' },
|
|
60
|
+
{ name: '3. ā Cancel this operation', value: 'cancel' },
|
|
61
|
+
{ name: '4. š Show more details and decide', value: 'details' }
|
|
62
|
+
],
|
|
63
|
+
default: 'approve'
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
switch (choice) {
|
|
67
|
+
case 'approve':
|
|
68
|
+
logger.info(`ā
User approved: ${operation} ${filePath}`);
|
|
69
|
+
return true;
|
|
70
|
+
case 'approve_session':
|
|
71
|
+
approvedOperationsForSession.add(operationType);
|
|
72
|
+
logger.info(`š "${operationType}" approved for session - all future ${operation} operations will auto-execute`);
|
|
73
|
+
logger.info(`ā
User approved: ${operation} ${filePath}`);
|
|
74
|
+
return true;
|
|
75
|
+
case 'cancel':
|
|
76
|
+
logger.info(`ā User cancelled: ${operation} ${filePath}`);
|
|
77
|
+
return false;
|
|
78
|
+
case 'details':
|
|
79
|
+
await showFileOperationDetails(context);
|
|
80
|
+
// Recursively ask again after showing details
|
|
81
|
+
return await requestFileOperationApproval(context);
|
|
82
|
+
default:
|
|
83
|
+
logger.info(`ā User cancelled: ${operation} ${filePath}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Show detailed information about the file operation
|
|
89
|
+
*/
|
|
90
|
+
async function showFileOperationDetails(context) {
|
|
91
|
+
const { operation, filePath, description, contentPreview } = context;
|
|
92
|
+
console.log(`\nš Detailed File Operation Information:`);
|
|
93
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
94
|
+
console.log(`š§ Operation Type: ${operation.toUpperCase()}`);
|
|
95
|
+
console.log(`š Target File: ${filePath}`);
|
|
96
|
+
console.log(`š Description: ${description}`);
|
|
97
|
+
console.log(`š Working Directory: ${process.cwd()}`);
|
|
98
|
+
// Show file status
|
|
99
|
+
try {
|
|
100
|
+
const fs = await import('fs/promises');
|
|
101
|
+
const stats = await fs.stat(filePath);
|
|
102
|
+
console.log(`š File exists: YES`);
|
|
103
|
+
console.log(`š
Last modified: ${stats.mtime.toLocaleString()}`);
|
|
104
|
+
console.log(`š File size: ${stats.size} bytes`);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.log(`š File exists: NO (will be created)`);
|
|
108
|
+
}
|
|
109
|
+
// Show full content if available
|
|
110
|
+
if (contentPreview) {
|
|
111
|
+
console.log(`\nš Full Content Preview:`);
|
|
112
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
113
|
+
console.log(contentPreview);
|
|
114
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
115
|
+
}
|
|
116
|
+
// Show security information
|
|
117
|
+
console.log(`\nš Security Information:`);
|
|
118
|
+
console.log(`⢠File path is validated and restricted to working directory`);
|
|
119
|
+
console.log(`⢠Operation will be performed atomically with backup`);
|
|
120
|
+
console.log(`⢠You can undo changes using git if needed`);
|
|
121
|
+
console.log(`\nš” Recommendation:`);
|
|
122
|
+
if (operation === 'write' && contentPreview) {
|
|
123
|
+
console.log(`⢠WRITE operation will ${contentPreview.includes('export') ? 'create a new file' : 'replace file content'}`);
|
|
124
|
+
console.log(`⢠Consider approving if the content looks correct`);
|
|
125
|
+
}
|
|
126
|
+
else if (operation === 'edit') {
|
|
127
|
+
console.log(`⢠EDIT operation will make targeted changes to existing file`);
|
|
128
|
+
console.log(`⢠Generally safer than write operations`);
|
|
129
|
+
}
|
|
130
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get current session approval status
|
|
134
|
+
*/
|
|
135
|
+
export function getSessionApprovalStatus() {
|
|
136
|
+
return {
|
|
137
|
+
writeApproved: approvedOperationsForSession.has('write_file'),
|
|
138
|
+
editApproved: approvedOperationsForSession.has('edit_file'),
|
|
139
|
+
dangerousMode: dangerouslyAcceptAll
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear session approvals
|
|
144
|
+
*/
|
|
145
|
+
export function clearSessionApprovals() {
|
|
146
|
+
approvedOperationsForSession.clear();
|
|
147
|
+
logger.info('š Session approvals cleared');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Show current approval status
|
|
151
|
+
*/
|
|
152
|
+
export function showApprovalStatus() {
|
|
153
|
+
const status = getSessionApprovalStatus();
|
|
154
|
+
console.log('\nš Current File Operation Approval Status:');
|
|
155
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
156
|
+
console.log(`š Dangerous Mode (auto-approve all): ${status.dangerousMode ? 'ā
ENABLED' : 'ā Disabled'}`);
|
|
157
|
+
console.log(`āļø Edit operations for session: ${status.editApproved ? 'ā
Auto-approved' : 'ā Require approval'}`);
|
|
158
|
+
console.log(`š Write operations for session: ${status.writeApproved ? 'ā
Auto-approved' : 'ā Require approval'}`);
|
|
159
|
+
if (status.dangerousMode) {
|
|
160
|
+
console.log(`\nā ļø WARNING: Dangerous mode is enabled - all file operations will be auto-approved!`);
|
|
161
|
+
}
|
|
162
|
+
console.log(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n`);
|
|
163
|
+
}
|