protoagent 0.0.5 → 0.1.0

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.
Files changed (58) hide show
  1. package/README.md +99 -19
  2. package/dist/App.js +602 -0
  3. package/dist/agentic-loop.js +492 -525
  4. package/dist/cli.js +39 -0
  5. package/dist/components/CollapsibleBox.js +26 -0
  6. package/dist/components/ConfigDialog.js +40 -0
  7. package/dist/components/ConsolidatedToolMessage.js +41 -0
  8. package/dist/components/FormattedMessage.js +93 -0
  9. package/dist/components/Table.js +275 -0
  10. package/dist/config.js +171 -0
  11. package/dist/mcp.js +170 -0
  12. package/dist/providers.js +137 -0
  13. package/dist/sessions.js +161 -0
  14. package/dist/skills.js +229 -0
  15. package/dist/sub-agent.js +103 -0
  16. package/dist/system-prompt.js +131 -0
  17. package/dist/tools/bash.js +178 -0
  18. package/dist/tools/edit-file.js +65 -171
  19. package/dist/tools/index.js +79 -134
  20. package/dist/tools/list-directory.js +20 -73
  21. package/dist/tools/read-file.js +57 -101
  22. package/dist/tools/search-files.js +74 -162
  23. package/dist/tools/todo.js +57 -140
  24. package/dist/tools/webfetch.js +310 -0
  25. package/dist/tools/write-file.js +44 -135
  26. package/dist/utils/approval.js +69 -0
  27. package/dist/utils/compactor.js +87 -0
  28. package/dist/utils/cost-tracker.js +26 -81
  29. package/dist/utils/format-message.js +26 -0
  30. package/dist/utils/logger.js +101 -307
  31. package/dist/utils/path-validation.js +74 -0
  32. package/package.json +45 -51
  33. package/LICENSE +0 -21
  34. package/dist/config/client.js +0 -315
  35. package/dist/config/commands.js +0 -223
  36. package/dist/config/manager.js +0 -117
  37. package/dist/config/mcp-commands.js +0 -266
  38. package/dist/config/mcp-manager.js +0 -240
  39. package/dist/config/mcp-types.js +0 -28
  40. package/dist/config/providers.js +0 -229
  41. package/dist/config/setup.js +0 -209
  42. package/dist/config/system-prompt.js +0 -397
  43. package/dist/config/types.js +0 -4
  44. package/dist/index.js +0 -229
  45. package/dist/tools/create-directory.js +0 -76
  46. package/dist/tools/directory-operations.js +0 -195
  47. package/dist/tools/file-operations.js +0 -211
  48. package/dist/tools/run-shell-command.js +0 -746
  49. package/dist/tools/search-operations.js +0 -179
  50. package/dist/tools/shell-operations.js +0 -342
  51. package/dist/tools/task-complete.js +0 -26
  52. package/dist/tools/view-directory-tree.js +0 -125
  53. package/dist/tools.js +0 -2
  54. package/dist/utils/conversation-compactor.js +0 -140
  55. package/dist/utils/enhanced-prompt.js +0 -23
  56. package/dist/utils/file-operations-approval.js +0 -373
  57. package/dist/utils/interrupt-handler.js +0 -127
  58. package/dist/utils/user-cancellation.js +0 -34
@@ -1,140 +0,0 @@
1
- /**
2
- * Conversation history compaction utilities
3
- */
4
- import { logger } from './logger.js';
5
- /**
6
- * Provides the system prompt for the history compression process.
7
- * This prompt instructs the model to act as a specialized state manager,
8
- * think in a scratchpad, and produce a structured XML summary.
9
- */
10
- export function getCompressionPrompt() {
11
- return `
12
- You are the component that summarizes internal chat history into a given structure.
13
-
14
- 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.
15
-
16
- 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.
17
-
18
- After your reasoning is complete, generate the final <state_snapshot> XML object. Be incredibly dense with information. Omit any irrelevant conversational filler.
19
-
20
- The structure MUST be as follows:
21
-
22
- <state_snapshot>
23
- <overall_goal>
24
- <!-- A single, concise sentence describing the user's high-level objective. -->
25
- <!-- Example: "Refactor the authentication service to use a new JWT library." -->
26
- </overall_goal>
27
-
28
- <key_knowledge>
29
- <!-- Crucial facts, conventions, and constraints the agent must remember based on the conversation history and interaction with the user. Use bullet points. -->
30
- <!-- Example:
31
- - Build Command: \`npm run build\`
32
- - Testing: Tests are run with \`npm test\`. Test files must end in \`.test.ts\`.
33
- - API Endpoint: The primary API endpoint is \`https://api.example.com/v2\`.
34
-
35
- -->
36
- </key_knowledge>
37
-
38
- <file_system_state>
39
- <!-- List files that have been created, read, modified, or deleted. Note their status and critical learnings. -->
40
- <!-- Example:
41
- - CWD: \`/home/user/project/src\`
42
- - READ: \`package.json\` - Confirmed 'axios' is a dependency.
43
- - MODIFIED: \`services/auth.ts\` - Replaced 'jsonwebtoken' with 'jose'.
44
- - CREATED: \`tests/new-feature.test.ts\` - Initial test structure for the new feature.
45
- -->
46
- </file_system_state>
47
-
48
- <recent_actions>
49
- <!-- A summary of the last few significant agent actions and their outcomes. Focus on facts. -->
50
- <!-- Example:
51
- - Ran \`grep 'old_function'\` which returned 3 results in 2 files.
52
- - Ran \`npm run test\`, which failed due to a snapshot mismatch in \`UserProfile.test.ts\`.
53
- - Ran \`ls -F static/\` and discovered image assets are stored as \`.webp\`.
54
- -->
55
- </recent_actions>
56
-
57
- <current_plan>
58
- <!-- The agent's step-by-step plan. Mark completed steps. -->
59
- <!-- Example:
60
- 1. [DONE] Identify all files using the deprecated 'UserAPI'.
61
- 2. [IN PROGRESS] Refactor \`src/components/UserProfile.tsx\` to use the new 'ProfileAPI'.
62
- 3. [TODO] Refactor the remaining files.
63
- 4. [TODO] Update tests to reflect the API change.
64
- -->
65
- </current_plan>
66
- </state_snapshot>
67
- `.trim();
68
- }
69
- /**
70
- * Compact conversation history by creating a summary
71
- */
72
- export async function compactConversation(client, model, messages, recentMessagesToKeep = 5) {
73
- logger.consoleLog('\nšŸ—œļø Compacting conversation history to manage context window...');
74
- // Keep the system message and recent messages
75
- const systemMessage = messages.find(m => m.role === 'system');
76
- const recentMessages = messages.slice(-recentMessagesToKeep);
77
- // Get the history to compress (excluding system and recent messages)
78
- const historyToCompress = messages.slice(systemMessage ? 1 : 0, -recentMessagesToKeep);
79
- if (historyToCompress.length === 0) {
80
- logger.consoleLog(' No history to compress, keeping current messages');
81
- return messages;
82
- }
83
- try {
84
- // Create compression request
85
- const compressionMessages = [
86
- {
87
- role: 'system',
88
- content: getCompressionPrompt()
89
- },
90
- {
91
- role: 'user',
92
- content: `Please compress the following conversation history:\n\n${JSON.stringify(historyToCompress, null, 2)}`
93
- }
94
- ];
95
- // Get compression summary (non-streaming for simplicity)
96
- const response = await client.chat.completions.create({
97
- model,
98
- messages: compressionMessages,
99
- max_tokens: 2000,
100
- temperature: 0.1
101
- });
102
- const compressionSummary = response.choices[0]?.message?.content;
103
- if (!compressionSummary) {
104
- logger.consoleLog(' Failed to generate compression summary, keeping original messages');
105
- return messages;
106
- }
107
- // Create the compressed history message
108
- const compressedMessage = {
109
- role: 'system',
110
- content: `Previous conversation summary:\n\n${compressionSummary}`
111
- };
112
- // Build new message array
113
- const compactedMessages = [];
114
- // Add original system message if it exists
115
- if (systemMessage) {
116
- compactedMessages.push(systemMessage);
117
- }
118
- // Add the compressed history
119
- compactedMessages.push(compressedMessage);
120
- // Add recent messages
121
- compactedMessages.push(...recentMessages);
122
- logger.consoleLog(` āœ… Compressed ${historyToCompress.length} messages into summary`);
123
- logger.consoleLog(` šŸ“ Keeping ${recentMessages.length} recent messages`);
124
- return compactedMessages;
125
- }
126
- catch (error) {
127
- logger.consoleLog(` āŒ Compression failed: ${error}. Keeping original messages.`);
128
- return messages;
129
- }
130
- }
131
- /**
132
- * Check if conversation needs compaction and perform it if necessary
133
- */
134
- export async function checkAndCompactIfNeeded(client, model, messages, maxTokens, currentTokens) {
135
- const utilizationPercentage = (currentTokens / maxTokens) * 100;
136
- if (utilizationPercentage >= 90) {
137
- return await compactConversation(client, model, messages);
138
- }
139
- return messages;
140
- }
@@ -1,23 +0,0 @@
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
- }
@@ -1,373 +0,0 @@
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
- import { UserCancellationError } from './user-cancellation.js';
8
- import { enhancedPrompt } from './enhanced-prompt.js';
9
- // Global state for approval settings
10
- let globalConfig = null;
11
- let dangerouslyAcceptAll = false;
12
- let approvedOperationsForSession = new Set();
13
- export function setFileOperationConfig(config) {
14
- globalConfig = config;
15
- }
16
- export function setDangerouslyAcceptAllFileOps(accept) {
17
- dangerouslyAcceptAll = accept;
18
- if (accept) {
19
- logger.warn('āš ļø DANGER MODE: All file operations will be auto-approved without confirmation!');
20
- }
21
- }
22
- /**
23
- * Request user approval for file write/edit operations
24
- * @throws {UserCancellationError} when user cancels the operation
25
- */
26
- export async function requestFileOperationApproval(context) {
27
- const { operation, filePath, description, contentPreview, oldContent, newContent, changeContext } = context;
28
- // Check if we should auto-approve
29
- if (dangerouslyAcceptAll) {
30
- logger.info(`šŸš€ Auto-approving (--dangerously-accept-all): ${operation} ${filePath}`);
31
- return;
32
- }
33
- const operationType = operation === 'write' ? 'write_file' : 'edit_file';
34
- if (approvedOperationsForSession.has(operationType)) {
35
- logger.info(`šŸš€ Auto-approving (${operationType} approved for session): ${operation} ${filePath}`);
36
- return;
37
- }
38
- // Show detailed preview of the operation
39
- await showOperationPreview(context);
40
- // Ask for user approval with enhanced options
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');
47
- switch (choice) {
48
- case 'approve':
49
- logger.info(`āœ… User approved: ${operation} ${filePath}`);
50
- return;
51
- case 'approve_session':
52
- approvedOperationsForSession.add(operationType);
53
- logger.info(`šŸ”“ "${operationType}" approved for session - all future ${operation} operations will auto-execute`);
54
- logger.info(`āœ… User approved: ${operation} ${filePath}`);
55
- return;
56
- case 'cancel':
57
- logger.info(`āŒ User cancelled: ${operation} ${filePath}`);
58
- throw new UserCancellationError(`${operation} operation on ${filePath}`, 'User chose to cancel the operation');
59
- case 'details':
60
- await showFileOperationDetails(context);
61
- // Recursively ask again after showing details
62
- return await requestFileOperationApproval(context);
63
- default:
64
- logger.info(`āŒ User cancelled: ${operation} ${filePath}`);
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
- }
294
- }
295
- return 'Text Content';
296
- }
297
- /**
298
- * Show detailed information about the file operation
299
- */
300
- async function showFileOperationDetails(context) {
301
- const { operation, filePath, description, contentPreview } = context;
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()}`);
308
- // Show file status
309
- try {
310
- const fs = await import('fs/promises');
311
- const stats = await fs.stat(filePath);
312
- logger.consoleLog(`šŸ“Š File exists: YES`);
313
- logger.consoleLog(`šŸ“… Last modified: ${stats.mtime.toLocaleString()}`);
314
- logger.consoleLog(`šŸ“ File size: ${stats.size} bytes`);
315
- }
316
- catch (error) {
317
- logger.consoleLog(`šŸ“Š File exists: NO (will be created)`);
318
- }
319
- // Show full content if available
320
- if (contentPreview) {
321
- logger.consoleLog(`\nšŸ“„ Full Content Preview:`);
322
- logger.consoleLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
323
- logger.consoleLog(contentPreview);
324
- logger.consoleLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
325
- }
326
- // Show security information
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:`);
332
- if (operation === 'write' && contentPreview) {
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`);
335
- }
336
- else if (operation === 'edit') {
337
- logger.consoleLog(`• EDIT operation will make targeted changes to existing file`);
338
- logger.consoleLog(`• Generally safer than write operations`);
339
- }
340
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
341
- }
342
- /**
343
- * Get current session approval status
344
- */
345
- export function getSessionApprovalStatus() {
346
- return {
347
- writeApproved: approvedOperationsForSession.has('write_file'),
348
- editApproved: approvedOperationsForSession.has('edit_file'),
349
- dangerousMode: dangerouslyAcceptAll
350
- };
351
- }
352
- /**
353
- * Clear session approvals
354
- */
355
- export function clearSessionApprovals() {
356
- approvedOperationsForSession.clear();
357
- logger.info('šŸ”„ Session approvals cleared');
358
- }
359
- /**
360
- * Show current approval status
361
- */
362
- export function showApprovalStatus() {
363
- const status = getSessionApprovalStatus();
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'}`);
369
- if (status.dangerousMode) {
370
- logger.consoleLog(`\nāš ļø WARNING: Dangerous mode is enabled - all file operations will be auto-approved!`);
371
- }
372
- logger.consoleLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
373
- }
@@ -1,127 +0,0 @@
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
- }