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.
@@ -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
- console.log('\nšŸ—œļø Compacting conversation history to manage context window...');
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
- console.log(' No history to compress, keeping current messages');
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
- console.log(' Failed to generate compression summary, keeping original messages');
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
- console.log(` āœ… Compressed ${historyToCompress.length} messages into summary`);
122
- console.log(` šŸ“ Keeping ${recentMessages.length} recent messages`);
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
- console.log(` āŒ Compression failed: ${error}. Keeping original messages.`);
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
- 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)}%)`);
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
- console.log(`āš ļø Context approaching limit - automatic compaction will trigger soon`);
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 true;
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 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
- }
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 { 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
- ]);
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 true;
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 true;
55
+ return;
75
56
  case 'cancel':
76
57
  logger.info(`āŒ User cancelled: ${operation} ${filePath}`);
77
- return false;
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
- return false;
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
- 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()}`);
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
- console.log(`šŸ“Š File exists: YES`);
103
- console.log(`šŸ“… Last modified: ${stats.mtime.toLocaleString()}`);
104
- console.log(`šŸ“ File size: ${stats.size} bytes`);
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
- console.log(`šŸ“Š File exists: NO (will be created)`);
317
+ logger.consoleLog(`šŸ“Š File exists: NO (will be created)`);
108
318
  }
109
319
  // Show full content if available
110
320
  if (contentPreview) {
111
- console.log(`\nšŸ“„ Full Content Preview:`);
112
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
113
- console.log(contentPreview);
114
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
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
- 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:`);
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
- 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`);
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
- console.log(`• EDIT operation will make targeted changes to existing file`);
128
- console.log(`• Generally safer than write operations`);
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
- 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'}`);
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
- console.log(`\nāš ļø WARNING: Dangerous mode is enabled - all file operations will be auto-approved!`);
370
+ logger.consoleLog(`\nāš ļø WARNING: Dangerous mode is enabled - all file operations will be auto-approved!`);
161
371
  }
162
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
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
+ }