protoagent 0.0.2 ā 0.0.4
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/dist/agentic-loop.js +379 -88
- package/dist/config/client.js +168 -19
- package/dist/config/commands.js +44 -29
- package/dist/config/providers.js +59 -0
- package/dist/config/setup.js +55 -21
- package/dist/config/system-prompt.js +117 -21
- package/dist/index.js +84 -17
- package/dist/tools/edit-file.js +43 -6
- package/dist/tools/index.js +81 -24
- package/dist/tools/run-shell-command.js +444 -38
- package/dist/tools/task-complete.js +26 -0
- package/dist/tools/write-file.js +15 -7
- package/dist/utils/conversation-compactor.js +7 -6
- package/dist/utils/cost-tracker.js +5 -4
- package/dist/utils/enhanced-prompt.js +23 -0
- package/dist/utils/file-operations-approval.js +277 -67
- package/dist/utils/interrupt-handler.js +127 -0
- package/dist/utils/logger.js +176 -3
- package/dist/utils/user-cancellation.js +34 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation history compaction utilities
|
|
3
3
|
*/
|
|
4
|
+
import { logger } from './logger.js';
|
|
4
5
|
/**
|
|
5
6
|
* Provides the system prompt for the history compression process.
|
|
6
7
|
* This prompt instructs the model to act as a specialized state manager,
|
|
@@ -69,14 +70,14 @@ The structure MUST be as follows:
|
|
|
69
70
|
* Compact conversation history by creating a summary
|
|
70
71
|
*/
|
|
71
72
|
export async function compactConversation(client, model, messages, recentMessagesToKeep = 5) {
|
|
72
|
-
|
|
73
|
+
logger.consoleLog('\nšļø Compacting conversation history to manage context window...');
|
|
73
74
|
// Keep the system message and recent messages
|
|
74
75
|
const systemMessage = messages.find(m => m.role === 'system');
|
|
75
76
|
const recentMessages = messages.slice(-recentMessagesToKeep);
|
|
76
77
|
// Get the history to compress (excluding system and recent messages)
|
|
77
78
|
const historyToCompress = messages.slice(systemMessage ? 1 : 0, -recentMessagesToKeep);
|
|
78
79
|
if (historyToCompress.length === 0) {
|
|
79
|
-
|
|
80
|
+
logger.consoleLog(' No history to compress, keeping current messages');
|
|
80
81
|
return messages;
|
|
81
82
|
}
|
|
82
83
|
try {
|
|
@@ -100,7 +101,7 @@ export async function compactConversation(client, model, messages, recentMessage
|
|
|
100
101
|
});
|
|
101
102
|
const compressionSummary = response.choices[0]?.message?.content;
|
|
102
103
|
if (!compressionSummary) {
|
|
103
|
-
|
|
104
|
+
logger.consoleLog(' Failed to generate compression summary, keeping original messages');
|
|
104
105
|
return messages;
|
|
105
106
|
}
|
|
106
107
|
// Create the compressed history message
|
|
@@ -118,12 +119,12 @@ export async function compactConversation(client, model, messages, recentMessage
|
|
|
118
119
|
compactedMessages.push(compressedMessage);
|
|
119
120
|
// Add recent messages
|
|
120
121
|
compactedMessages.push(...recentMessages);
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
logger.consoleLog(` ā
Compressed ${historyToCompress.length} messages into summary`);
|
|
123
|
+
logger.consoleLog(` š Keeping ${recentMessages.length} recent messages`);
|
|
123
124
|
return compactedMessages;
|
|
124
125
|
}
|
|
125
126
|
catch (error) {
|
|
126
|
-
|
|
127
|
+
logger.consoleLog(` ā Compression failed: ${error}. Keeping original messages.`);
|
|
127
128
|
return messages;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cost tracking and token counting utilities
|
|
3
3
|
*/
|
|
4
|
+
import { logger } from './logger.js';
|
|
4
5
|
/**
|
|
5
6
|
* Rough token estimation for OpenAI models
|
|
6
7
|
* This is approximate - actual tokenization may vary
|
|
@@ -72,11 +73,11 @@ export function getContextInfo(messages, modelConfig) {
|
|
|
72
73
|
* Log usage and cost information
|
|
73
74
|
*/
|
|
74
75
|
export function logUsageInfo(usage, contextInfo, modelConfig) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
logger.consoleLog(`š° Usage: ${usage.inputTokens} in + ${usage.outputTokens} out = ${usage.totalTokens} tokens`);
|
|
77
|
+
logger.consoleLog(`šø Estimated cost: $${usage.estimatedCost.toFixed(6)}`);
|
|
78
|
+
logger.consoleLog(`š Context: ${contextInfo.currentTokens}/${contextInfo.maxTokens} tokens (${contextInfo.utilizationPercentage.toFixed(1)}%)`);
|
|
78
79
|
if (contextInfo.needsCompaction) {
|
|
79
|
-
|
|
80
|
+
logger.consoleLog(`ā ļø Context approaching limit - automatic compaction will trigger soon`);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced prompt utility that supports both arrow key navigation + enter
|
|
3
|
+
* AND displays clear number shortcuts for faster interaction
|
|
4
|
+
*/
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced prompt that uses standard inquirer with clear number indicators
|
|
8
|
+
* Users can still use arrow keys + enter as normal, but numbers make it clear what to select
|
|
9
|
+
*/
|
|
10
|
+
export async function enhancedPrompt(message, choices, defaultValue) {
|
|
11
|
+
// Add instruction about keyboard shortcuts
|
|
12
|
+
const enhancedMessage = `${message}\nš” Use arrow keys + Enter, or press the number key + Enter`;
|
|
13
|
+
const result = await inquirer.prompt([
|
|
14
|
+
{
|
|
15
|
+
type: 'list',
|
|
16
|
+
name: 'choice',
|
|
17
|
+
message: enhancedMessage,
|
|
18
|
+
choices: choices,
|
|
19
|
+
default: defaultValue
|
|
20
|
+
}
|
|
21
|
+
]);
|
|
22
|
+
return result.choice;
|
|
23
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
|
+
import { UserCancellationError } from './user-cancellation.js';
|
|
8
|
+
import { enhancedPrompt } from './enhanced-prompt.js';
|
|
7
9
|
// Global state for approval settings
|
|
8
10
|
let globalConfig = null;
|
|
9
11
|
let dangerouslyAcceptAll = false;
|
|
@@ -19,113 +21,321 @@ export function setDangerouslyAcceptAllFileOps(accept) {
|
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Request user approval for file write/edit operations
|
|
24
|
+
* @throws {UserCancellationError} when user cancels the operation
|
|
22
25
|
*/
|
|
23
26
|
export async function requestFileOperationApproval(context) {
|
|
24
|
-
const { operation, filePath, description, contentPreview } = context;
|
|
27
|
+
const { operation, filePath, description, contentPreview, oldContent, newContent, changeContext } = context;
|
|
25
28
|
// Check if we should auto-approve
|
|
26
29
|
if (dangerouslyAcceptAll) {
|
|
27
30
|
logger.info(`š Auto-approving (--dangerously-accept-all): ${operation} ${filePath}`);
|
|
28
|
-
return
|
|
31
|
+
return;
|
|
29
32
|
}
|
|
30
33
|
const operationType = operation === 'write' ? 'write_file' : 'edit_file';
|
|
31
34
|
if (approvedOperationsForSession.has(operationType)) {
|
|
32
35
|
logger.info(`š Auto-approving (${operationType} approved for session): ${operation} ${filePath}`);
|
|
33
|
-
return
|
|
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
|
-
}
|
|
36
|
+
return;
|
|
50
37
|
}
|
|
38
|
+
// Show detailed preview of the operation
|
|
39
|
+
await showOperationPreview(context);
|
|
51
40
|
// Ask for user approval with enhanced options
|
|
52
|
-
const
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
]);
|
|
41
|
+
const choice = await enhancedPrompt('Choose your action:', [
|
|
42
|
+
{ name: `1. ā
Approve this ${operation} operation`, value: 'approve' },
|
|
43
|
+
{ name: `2. ā
Approve and allow all ${operation} operations for this session`, value: 'approve_session' },
|
|
44
|
+
{ name: '3. ā Cancel this operation', value: 'cancel' },
|
|
45
|
+
{ name: '4. š Show detailed analysis', value: 'details' }
|
|
46
|
+
], 'approve');
|
|
66
47
|
switch (choice) {
|
|
67
48
|
case 'approve':
|
|
68
49
|
logger.info(`ā
User approved: ${operation} ${filePath}`);
|
|
69
|
-
return
|
|
50
|
+
return;
|
|
70
51
|
case 'approve_session':
|
|
71
52
|
approvedOperationsForSession.add(operationType);
|
|
72
53
|
logger.info(`š "${operationType}" approved for session - all future ${operation} operations will auto-execute`);
|
|
73
54
|
logger.info(`ā
User approved: ${operation} ${filePath}`);
|
|
74
|
-
return
|
|
55
|
+
return;
|
|
75
56
|
case 'cancel':
|
|
76
57
|
logger.info(`ā User cancelled: ${operation} ${filePath}`);
|
|
77
|
-
|
|
58
|
+
throw new UserCancellationError(`${operation} operation on ${filePath}`, 'User chose to cancel the operation');
|
|
78
59
|
case 'details':
|
|
79
60
|
await showFileOperationDetails(context);
|
|
80
61
|
// Recursively ask again after showing details
|
|
81
62
|
return await requestFileOperationApproval(context);
|
|
82
63
|
default:
|
|
83
64
|
logger.info(`ā User cancelled: ${operation} ${filePath}`);
|
|
84
|
-
|
|
65
|
+
throw new UserCancellationError(`${operation} operation on ${filePath}`, 'User did not approve the operation');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Show a detailed preview of the file operation before asking for approval
|
|
70
|
+
*/
|
|
71
|
+
async function showOperationPreview(context) {
|
|
72
|
+
const { operation, filePath, description, contentPreview, oldContent, newContent, changeContext } = context;
|
|
73
|
+
logger.consoleLog(`\nš File Operation Preview: ${operation.toUpperCase()}`);
|
|
74
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
75
|
+
logger.consoleLog(`š File: ${filePath}`);
|
|
76
|
+
logger.consoleLog(`š Action: ${description}`);
|
|
77
|
+
// Show risk assessment
|
|
78
|
+
const riskAssessment = analyzeOperationRisk(context);
|
|
79
|
+
const riskColor = riskAssessment.level === 'LOW' ? 'š¢' : riskAssessment.level === 'MEDIUM' ? 'š”' : 'š“';
|
|
80
|
+
logger.consoleLog(`${riskColor} Risk Level: ${riskAssessment.level} - ${riskAssessment.reason}`);
|
|
81
|
+
if (operation === 'edit' && oldContent && newContent) {
|
|
82
|
+
// Show diff preview for edit operations
|
|
83
|
+
logger.consoleLog(`\nš Change Summary:`);
|
|
84
|
+
if (changeContext) {
|
|
85
|
+
logger.consoleLog(` ⢠Lines added: ${changeContext.linesAdded}`);
|
|
86
|
+
logger.consoleLog(` ⢠Lines removed: ${changeContext.linesRemoved}`);
|
|
87
|
+
logger.consoleLog(` ⢠Total lines after change: ${changeContext.totalLines}`);
|
|
88
|
+
logger.consoleLog(` ⢠Affected line numbers: ${changeContext.affectedLineNumbers.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
logger.consoleLog(`\nš Diff Preview (showing changes):`);
|
|
91
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
92
|
+
showDiffPreview(oldContent, newContent);
|
|
93
|
+
}
|
|
94
|
+
else if (operation === 'write') {
|
|
95
|
+
// Show content preview for write operations
|
|
96
|
+
const contentLength = newContent?.length || contentPreview?.length || 0;
|
|
97
|
+
logger.consoleLog(`\nš Content Summary:`);
|
|
98
|
+
logger.consoleLog(` ⢠Content length: ${contentLength} characters`);
|
|
99
|
+
if (newContent || contentPreview) {
|
|
100
|
+
const content = newContent || contentPreview || '';
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
logger.consoleLog(` ⢠Total lines: ${lines.length}`);
|
|
103
|
+
// Detect content type
|
|
104
|
+
const contentType = detectContentType(filePath, content);
|
|
105
|
+
logger.consoleLog(` ⢠Detected type: ${contentType}`);
|
|
106
|
+
logger.consoleLog(`\nš Content Preview (first/last 10 lines):`);
|
|
107
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
108
|
+
showContentPreview(content);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Show a side-by-side or unified diff preview
|
|
115
|
+
*/
|
|
116
|
+
function showDiffPreview(oldContent, newContent) {
|
|
117
|
+
const oldLines = oldContent.split('\n');
|
|
118
|
+
const newLines = newContent.split('\n');
|
|
119
|
+
// Simple line-by-line diff (could be enhanced with proper diff algorithm)
|
|
120
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
121
|
+
const contextLines = 3; // Show 3 lines of context around changes
|
|
122
|
+
let changes = [];
|
|
123
|
+
// Detect changes (simplified diff)
|
|
124
|
+
for (let i = 0; i < maxLines; i++) {
|
|
125
|
+
const oldLine = i < oldLines.length ? oldLines[i] : undefined;
|
|
126
|
+
const newLine = i < newLines.length ? newLines[i] : undefined;
|
|
127
|
+
if (oldLine === newLine) {
|
|
128
|
+
changes.push({ lineNum: i + 1, type: 'unchanged', oldLine, newLine });
|
|
129
|
+
}
|
|
130
|
+
else if (oldLine === undefined) {
|
|
131
|
+
changes.push({ lineNum: i + 1, type: 'added', newLine });
|
|
132
|
+
}
|
|
133
|
+
else if (newLine === undefined) {
|
|
134
|
+
changes.push({ lineNum: i + 1, type: 'removed', oldLine });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
changes.push({ lineNum: i + 1, type: 'removed', oldLine });
|
|
138
|
+
changes.push({ lineNum: i + 1, type: 'added', newLine });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Show relevant changes with context
|
|
142
|
+
const changedLineNums = changes
|
|
143
|
+
.filter(c => c.type !== 'unchanged')
|
|
144
|
+
.map(c => c.lineNum);
|
|
145
|
+
if (changedLineNums.length === 0) {
|
|
146
|
+
logger.consoleLog(` (No line changes detected)`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Show up to 20 lines of diff to avoid overwhelming output
|
|
150
|
+
const diffLines = changes.slice(0, 20);
|
|
151
|
+
for (const change of diffLines) {
|
|
152
|
+
const lineNumStr = change.lineNum.toString().padStart(4, ' ');
|
|
153
|
+
switch (change.type) {
|
|
154
|
+
case 'unchanged':
|
|
155
|
+
if (change.oldLine !== undefined) {
|
|
156
|
+
logger.consoleLog(` ${lineNumStr} ${change.oldLine}`);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
case 'removed':
|
|
160
|
+
logger.consoleLog(`- ${lineNumStr} ${change.oldLine || ''}`);
|
|
161
|
+
break;
|
|
162
|
+
case 'added':
|
|
163
|
+
logger.consoleLog(`+ ${lineNumStr} ${change.newLine || ''}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (changes.length > 20) {
|
|
168
|
+
logger.consoleLog(` ... (${changes.length - 20} more lines)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Show preview of file content
|
|
173
|
+
*/
|
|
174
|
+
function showContentPreview(content) {
|
|
175
|
+
const lines = content.split('\n');
|
|
176
|
+
const maxPreviewLines = 20;
|
|
177
|
+
if (lines.length <= maxPreviewLines) {
|
|
178
|
+
// Show all lines if content is short
|
|
179
|
+
lines.forEach((line, i) => {
|
|
180
|
+
const lineNum = (i + 1).toString().padStart(4, ' ');
|
|
181
|
+
logger.consoleLog(` ${lineNum} ${line}`);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Show first 10 and last 10 lines
|
|
186
|
+
const firstLines = lines.slice(0, 10);
|
|
187
|
+
const lastLines = lines.slice(-10);
|
|
188
|
+
firstLines.forEach((line, i) => {
|
|
189
|
+
const lineNum = (i + 1).toString().padStart(4, ' ');
|
|
190
|
+
logger.consoleLog(` ${lineNum} ${line}`);
|
|
191
|
+
});
|
|
192
|
+
logger.consoleLog(` ... (${lines.length - 20} lines omitted)`);
|
|
193
|
+
lastLines.forEach((line, i) => {
|
|
194
|
+
const lineNum = (lines.length - 10 + i + 1).toString().padStart(4, ' ');
|
|
195
|
+
logger.consoleLog(` ${lineNum} ${line}`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Analyze the risk level of a file operation
|
|
201
|
+
*/
|
|
202
|
+
function analyzeOperationRisk(context) {
|
|
203
|
+
const { operation, filePath, newContent, contentPreview } = context;
|
|
204
|
+
const content = newContent || contentPreview || '';
|
|
205
|
+
// Check for high-risk patterns
|
|
206
|
+
const highRiskPatterns = [
|
|
207
|
+
/package\.json.*"scripts"/s,
|
|
208
|
+
/\.env/i,
|
|
209
|
+
/docker/i,
|
|
210
|
+
/\.sh$/,
|
|
211
|
+
/\.bat$/,
|
|
212
|
+
/\.cmd$/,
|
|
213
|
+
/sudo|rm\s+-rf|chmod\s+777/,
|
|
214
|
+
/eval\s*\(|exec\s*\(/,
|
|
215
|
+
/process\.env\s*\[/
|
|
216
|
+
];
|
|
217
|
+
// Check for medium-risk patterns
|
|
218
|
+
const mediumRiskPatterns = [
|
|
219
|
+
/config/i,
|
|
220
|
+
/\.json$/,
|
|
221
|
+
/import.*from.*http/,
|
|
222
|
+
/require.*http/,
|
|
223
|
+
/fetch\s*\(/,
|
|
224
|
+
/\.key$|\.pem$|\.crt$/i
|
|
225
|
+
];
|
|
226
|
+
// Check file path risks
|
|
227
|
+
if (filePath.includes('node_modules')) {
|
|
228
|
+
return { level: 'HIGH', reason: 'Modifying node_modules can break dependencies' };
|
|
229
|
+
}
|
|
230
|
+
if (filePath.includes('.git/')) {
|
|
231
|
+
return { level: 'HIGH', reason: 'Modifying .git directory can corrupt repository' };
|
|
232
|
+
}
|
|
233
|
+
// Check content risks
|
|
234
|
+
for (const pattern of highRiskPatterns) {
|
|
235
|
+
if (pattern.test(content) || pattern.test(filePath)) {
|
|
236
|
+
return { level: 'HIGH', reason: 'Contains potentially dangerous operations or sensitive files' };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const pattern of mediumRiskPatterns) {
|
|
240
|
+
if (pattern.test(content) || pattern.test(filePath)) {
|
|
241
|
+
return { level: 'MEDIUM', reason: 'Contains configuration or network operations' };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Default to low risk for typical development files
|
|
245
|
+
if (operation === 'edit') {
|
|
246
|
+
return { level: 'LOW', reason: 'Targeted edit operation to existing file' };
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
return { level: 'LOW', reason: 'Standard file write operation' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Detect the type of content being written
|
|
254
|
+
*/
|
|
255
|
+
function detectContentType(filePath, content) {
|
|
256
|
+
const extension = filePath.split('.').pop()?.toLowerCase();
|
|
257
|
+
// File extension based detection
|
|
258
|
+
const extensionTypes = {
|
|
259
|
+
'js': 'JavaScript',
|
|
260
|
+
'ts': 'TypeScript',
|
|
261
|
+
'jsx': 'React JSX',
|
|
262
|
+
'tsx': 'React TSX',
|
|
263
|
+
'json': 'JSON Configuration',
|
|
264
|
+
'html': 'HTML Document',
|
|
265
|
+
'css': 'CSS Stylesheet',
|
|
266
|
+
'scss': 'SCSS Stylesheet',
|
|
267
|
+
'md': 'Markdown Document',
|
|
268
|
+
'txt': 'Text File',
|
|
269
|
+
'yml': 'YAML Configuration',
|
|
270
|
+
'yaml': 'YAML Configuration',
|
|
271
|
+
'xml': 'XML Document',
|
|
272
|
+
'py': 'Python Script',
|
|
273
|
+
'sh': 'Shell Script',
|
|
274
|
+
'bat': 'Batch Script'
|
|
275
|
+
};
|
|
276
|
+
if (extension && extensionTypes[extension]) {
|
|
277
|
+
return extensionTypes[extension];
|
|
278
|
+
}
|
|
279
|
+
// Content-based detection
|
|
280
|
+
if (content.includes('export') || content.includes('import')) {
|
|
281
|
+
return 'JavaScript/TypeScript Module';
|
|
282
|
+
}
|
|
283
|
+
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
284
|
+
return 'HTML Document';
|
|
285
|
+
}
|
|
286
|
+
if (content.includes('{') && content.includes('}')) {
|
|
287
|
+
try {
|
|
288
|
+
JSON.parse(content);
|
|
289
|
+
return 'JSON Data';
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Not valid JSON
|
|
293
|
+
}
|
|
85
294
|
}
|
|
295
|
+
return 'Text Content';
|
|
86
296
|
}
|
|
87
297
|
/**
|
|
88
298
|
* Show detailed information about the file operation
|
|
89
299
|
*/
|
|
90
300
|
async function showFileOperationDetails(context) {
|
|
91
301
|
const { operation, filePath, description, contentPreview } = context;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
302
|
+
logger.consoleLog(`\nš Detailed File Operation Information:`);
|
|
303
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
304
|
+
logger.consoleLog(`š§ Operation Type: ${operation.toUpperCase()}`);
|
|
305
|
+
logger.consoleLog(`š Target File: ${filePath}`);
|
|
306
|
+
logger.consoleLog(`š Description: ${description}`);
|
|
307
|
+
logger.consoleLog(`š Working Directory: ${process.cwd()}`);
|
|
98
308
|
// Show file status
|
|
99
309
|
try {
|
|
100
310
|
const fs = await import('fs/promises');
|
|
101
311
|
const stats = await fs.stat(filePath);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
312
|
+
logger.consoleLog(`š File exists: YES`);
|
|
313
|
+
logger.consoleLog(`š
Last modified: ${stats.mtime.toLocaleString()}`);
|
|
314
|
+
logger.consoleLog(`š File size: ${stats.size} bytes`);
|
|
105
315
|
}
|
|
106
316
|
catch (error) {
|
|
107
|
-
|
|
317
|
+
logger.consoleLog(`š File exists: NO (will be created)`);
|
|
108
318
|
}
|
|
109
319
|
// Show full content if available
|
|
110
320
|
if (contentPreview) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
321
|
+
logger.consoleLog(`\nš Full Content Preview:`);
|
|
322
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
323
|
+
logger.consoleLog(contentPreview);
|
|
324
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
115
325
|
}
|
|
116
326
|
// Show security information
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
327
|
+
logger.consoleLog(`\nš Security Information:`);
|
|
328
|
+
logger.consoleLog(`⢠File path is validated and restricted to working directory`);
|
|
329
|
+
logger.consoleLog(`⢠Operation will be performed atomically with backup`);
|
|
330
|
+
logger.consoleLog(`⢠You can undo changes using git if needed`);
|
|
331
|
+
logger.consoleLog(`\nš” Recommendation:`);
|
|
122
332
|
if (operation === 'write' && contentPreview) {
|
|
123
|
-
|
|
124
|
-
|
|
333
|
+
logger.consoleLog(`⢠WRITE operation will ${contentPreview.includes('export') ? 'create a new file' : 'replace file content'}`);
|
|
334
|
+
logger.consoleLog(`⢠Consider approving if the content looks correct`);
|
|
125
335
|
}
|
|
126
336
|
else if (operation === 'edit') {
|
|
127
|
-
|
|
128
|
-
|
|
337
|
+
logger.consoleLog(`⢠EDIT operation will make targeted changes to existing file`);
|
|
338
|
+
logger.consoleLog(`⢠Generally safer than write operations`);
|
|
129
339
|
}
|
|
130
340
|
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
131
341
|
}
|
|
@@ -151,13 +361,13 @@ export function clearSessionApprovals() {
|
|
|
151
361
|
*/
|
|
152
362
|
export function showApprovalStatus() {
|
|
153
363
|
const status = getSessionApprovalStatus();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
364
|
+
logger.consoleLog('\nš Current File Operation Approval Status:');
|
|
365
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`);
|
|
366
|
+
logger.consoleLog(`š Dangerous Mode (auto-approve all): ${status.dangerousMode ? 'ā
ENABLED' : 'ā Disabled'}`);
|
|
367
|
+
logger.consoleLog(`āļø Edit operations for session: ${status.editApproved ? 'ā
Auto-approved' : 'ā Require approval'}`);
|
|
368
|
+
logger.consoleLog(`š Write operations for session: ${status.writeApproved ? 'ā
Auto-approved' : 'ā Require approval'}`);
|
|
159
369
|
if (status.dangerousMode) {
|
|
160
|
-
|
|
370
|
+
logger.consoleLog(`\nā ļø WARNING: Dangerous mode is enabled - all file operations will be auto-approved!`);
|
|
161
371
|
}
|
|
162
|
-
|
|
372
|
+
logger.consoleLog(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n`);
|
|
163
373
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
export class UserInterruptError extends Error {
|
|
3
|
+
constructor(message = 'User paused operation') {
|
|
4
|
+
super(message);
|
|
5
|
+
this.isUserInterrupt = true;
|
|
6
|
+
this.name = 'UserInterruptError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class InterruptHandler {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.isListening = false;
|
|
12
|
+
this.interrupted = false;
|
|
13
|
+
this.readline = null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Start listening for 'q' key to interrupt
|
|
17
|
+
*/
|
|
18
|
+
startListening() {
|
|
19
|
+
if (this.isListening)
|
|
20
|
+
return;
|
|
21
|
+
this.isListening = true;
|
|
22
|
+
this.interrupted = false;
|
|
23
|
+
// Set stdin to raw mode to capture single key presses
|
|
24
|
+
if (process.stdin.isTTY) {
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
process.stdin.resume();
|
|
27
|
+
process.stdin.setEncoding('utf8');
|
|
28
|
+
const onKeyPress = (key) => {
|
|
29
|
+
if (key === 'q' || key === 'Q') {
|
|
30
|
+
this.interrupted = true;
|
|
31
|
+
logger.debug('š User pressed Q - interrupt signal received', { component: 'InterruptHandler' });
|
|
32
|
+
this.stopListening();
|
|
33
|
+
// Just show a simple newline - no dramatic messages
|
|
34
|
+
process.stdout.write('\n');
|
|
35
|
+
}
|
|
36
|
+
// Handle Ctrl+C gracefully
|
|
37
|
+
if (key === '\u0003') { // Ctrl+C
|
|
38
|
+
logger.debug('š User pressed Ctrl+C - graceful exit', { component: 'InterruptHandler' });
|
|
39
|
+
process.stdout.write('\n\nš Goodbye!\n');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
process.stdin.on('data', onKeyPress);
|
|
44
|
+
// Store the listener for cleanup
|
|
45
|
+
this._keyPressListener = onKeyPress;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Stop listening for interrupts
|
|
50
|
+
*/
|
|
51
|
+
stopListening() {
|
|
52
|
+
if (!this.isListening)
|
|
53
|
+
return;
|
|
54
|
+
this.isListening = false;
|
|
55
|
+
if (process.stdin.isTTY) {
|
|
56
|
+
// Remove the specific listener
|
|
57
|
+
if (this._keyPressListener) {
|
|
58
|
+
process.stdin.removeListener('data', this._keyPressListener);
|
|
59
|
+
this._keyPressListener = null;
|
|
60
|
+
}
|
|
61
|
+
// Reset stdin to normal mode
|
|
62
|
+
process.stdin.setRawMode(false);
|
|
63
|
+
process.stdin.pause();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if user has interrupted
|
|
68
|
+
*/
|
|
69
|
+
isInterrupted() {
|
|
70
|
+
return this.interrupted;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reset interrupt state
|
|
74
|
+
*/
|
|
75
|
+
reset() {
|
|
76
|
+
this.interrupted = false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Throw interrupt error if interrupted
|
|
80
|
+
*/
|
|
81
|
+
throwIfInterrupted() {
|
|
82
|
+
if (this.interrupted) {
|
|
83
|
+
logger.debug('User requested pause', { component: 'InterruptHandler' });
|
|
84
|
+
throw new UserInterruptError();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Global interrupt handler instance
|
|
89
|
+
export const interruptHandler = new InterruptHandler();
|
|
90
|
+
/**
|
|
91
|
+
* Convenience function to check for interrupt during async operations
|
|
92
|
+
*/
|
|
93
|
+
export function checkInterrupt() {
|
|
94
|
+
interruptHandler.throwIfInterrupted();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrapper for async operations that can be interrupted
|
|
98
|
+
*/
|
|
99
|
+
export async function interruptible(operation, checkInterval = 100) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
let completed = false;
|
|
102
|
+
// Start the operation
|
|
103
|
+
operation()
|
|
104
|
+
.then(result => {
|
|
105
|
+
completed = true;
|
|
106
|
+
resolve(result);
|
|
107
|
+
})
|
|
108
|
+
.catch(error => {
|
|
109
|
+
completed = true;
|
|
110
|
+
reject(error);
|
|
111
|
+
});
|
|
112
|
+
// Check for interrupts periodically
|
|
113
|
+
const checkInterrupts = () => {
|
|
114
|
+
if (completed)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
checkInterrupt();
|
|
118
|
+
setTimeout(checkInterrupts, checkInterval);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
completed = true;
|
|
122
|
+
reject(error);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
setTimeout(checkInterrupts, checkInterval);
|
|
126
|
+
});
|
|
127
|
+
}
|