recoder-code 2.3.4 → 2.3.6

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.
@@ -0,0 +1,230 @@
1
+ const readline = require('readline');
2
+ const chalkModule = require('chalk');
3
+ const chalk = chalkModule.default || chalkModule;
4
+
5
+ /**
6
+ * Approval Dialog System - Claude Code Style
7
+ * Multi-choice numbered options with preferences
8
+ */
9
+ class ApprovalDialog {
10
+ constructor(rl) {
11
+ this.rl = rl;
12
+ this.preferences = new Map(); // Store "don't ask again" preferences
13
+ }
14
+
15
+ /**
16
+ * Show bash command approval with multi-choice options
17
+ */
18
+ async showBashApproval(command, description = '') {
19
+ const width = 90;
20
+
21
+ // Build dialog
22
+ console.log('');
23
+ console.log(chalk.cyan('╭' + '─'.repeat(width - 2) + '╮'));
24
+ console.log(chalk.cyan('│') + chalk.bold(' Bash command') + ' '.repeat(width - 15) + chalk.cyan('│'));
25
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
26
+ console.log(chalk.cyan('│') + ' ' + chalk.white(this.truncate(command, width - 6)) + ' '.repeat(Math.max(0, width - 6 - command.length)) + chalk.cyan('│'));
27
+
28
+ if (description) {
29
+ console.log(chalk.cyan('│') + ' ' + chalk.dim(this.truncate(description, width - 6)) + ' '.repeat(Math.max(0, width - 6 - description.length)) + chalk.cyan('│'));
30
+ }
31
+
32
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
33
+ console.log(chalk.cyan('│') + chalk.white(' Do you want to proceed?') + ' '.repeat(width - 28) + chalk.cyan('│'));
34
+ console.log(chalk.cyan('│') + chalk.green(' ❯ 1. ') + chalk.white('Yes') + ' '.repeat(width - 13) + chalk.cyan('│'));
35
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('2. ') + chalk.white('Yes, and don\'t ask again for bash commands') + ' '.repeat(Math.max(0, width - 51)) + chalk.cyan('│'));
36
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('3. ') + chalk.white('No, and tell Claude what to do differently ') + chalk.dim('(esc)') + ' '.repeat(Math.max(0, width - 58)) + chalk.cyan('│'));
37
+ console.log(chalk.cyan('╰' + '─'.repeat(width - 2) + '╯'));
38
+ console.log('');
39
+
40
+ return this.waitForChoice(3);
41
+ }
42
+
43
+ /**
44
+ * Show file operation approval with multi-choice options
45
+ */
46
+ async showFileApproval(operation, filePath, diff, stats = {}) {
47
+ const width = 90;
48
+ const { additions = 0, deletions = 0 } = stats;
49
+
50
+ // Determine operation type
51
+ let title = 'File Operation';
52
+ let descText = '';
53
+
54
+ if (operation === 'create') {
55
+ title = 'File Creation';
56
+ descText = `Create ${filePath}`;
57
+ } else if (operation === 'str_replace') {
58
+ title = 'File Edit';
59
+ descText = `Modify ${filePath} (${additions > 0 ? '+' + additions : ''}${deletions > 0 ? ' -' + deletions : ''})`;
60
+ }
61
+
62
+ // Build dialog
63
+ console.log('');
64
+ console.log(chalk.cyan('╭' + '─'.repeat(width - 2) + '╮'));
65
+ console.log(chalk.cyan('│') + chalk.bold(` ${title}`) + ' '.repeat(width - title.length - 3) + chalk.cyan('│'));
66
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
67
+ console.log(chalk.cyan('│') + ' ' + chalk.white(this.truncate(descText, width - 6)) + ' '.repeat(Math.max(0, width - 6 - descText.length)) + chalk.cyan('│'));
68
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
69
+
70
+ // Show truncated diff
71
+ const diffLines = diff.split('\n').slice(0, 8);
72
+ diffLines.forEach(line => {
73
+ const displayLine = this.truncate(line, width - 6);
74
+ console.log(chalk.cyan('│') + ' ' + displayLine + ' '.repeat(Math.max(0, width - 4 - displayLine.length)) + chalk.cyan('│'));
75
+ });
76
+
77
+ if (diff.split('\n').length > 8) {
78
+ const remaining = diff.split('\n').length - 8;
79
+ console.log(chalk.cyan('│') + ' ' + chalk.yellow(`… +${remaining} lines `) + chalk.dim('(ctrl+o to expand)') + ' '.repeat(Math.max(0, width - 30 - remaining.toString().length)) + chalk.cyan('│'));
80
+ }
81
+
82
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
83
+ console.log(chalk.cyan('│') + chalk.white(' Do you want to proceed?') + ' '.repeat(width - 28) + chalk.cyan('│'));
84
+ console.log(chalk.cyan('│') + chalk.green(' ❯ 1. ') + chalk.white('Yes') + ' '.repeat(width - 13) + chalk.cyan('│'));
85
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('2. ') + chalk.white('Yes, and auto-accept all remaining changes') + ' '.repeat(Math.max(0, width - 50)) + chalk.cyan('│'));
86
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('3. ') + chalk.white('No, skip this change') + ' '.repeat(width - 28) + chalk.cyan('│'));
87
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('4. ') + chalk.white('No, and tell Claude what to change ') + chalk.dim('(esc)') + ' '.repeat(Math.max(0, width - 50)) + chalk.cyan('│'));
88
+ console.log(chalk.cyan('╰' + '─'.repeat(width - 2) + '╯'));
89
+ console.log('');
90
+
91
+ return this.waitForChoice(4);
92
+ }
93
+
94
+ /**
95
+ * Wait for user to select a numbered choice
96
+ */
97
+ async waitForChoice(maxChoice) {
98
+ return new Promise((resolve) => {
99
+ const handleInput = (input) => {
100
+ const choice = input.trim();
101
+
102
+ // Handle escape key
103
+ if (input === '\x1b') {
104
+ cleanup();
105
+ resolve({ choice: maxChoice, alternative: true }); // Last option is always "tell me what to do"
106
+ return;
107
+ }
108
+
109
+ // Handle numbered choices
110
+ const num = parseInt(choice);
111
+ if (num >= 1 && num <= maxChoice) {
112
+ cleanup();
113
+ resolve({ choice: num, alternative: false });
114
+ return;
115
+ }
116
+
117
+ // Invalid input - show again
118
+ process.stdout.write(chalk.red('Invalid choice. ') + chalk.dim(`Enter 1-${maxChoice}: `));
119
+ };
120
+
121
+ const cleanup = () => {
122
+ process.stdin.removeListener('data', handleInput);
123
+ };
124
+
125
+ // Listen for keypress
126
+ process.stdout.write(chalk.dim(`Enter choice (1-${maxChoice}): `));
127
+ process.stdin.once('data', handleInput);
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Show simple yes/no confirmation
133
+ */
134
+ async showConfirmation(message) {
135
+ const width = 90;
136
+
137
+ console.log('');
138
+ console.log(chalk.cyan('╭' + '─'.repeat(width - 2) + '╮'));
139
+ console.log(chalk.cyan('│') + ' ' + chalk.white(this.truncate(message, width - 6)) + ' '.repeat(Math.max(0, width - 6 - message.length)) + chalk.cyan('│'));
140
+ console.log(chalk.cyan('│') + ' '.repeat(width - 2) + chalk.cyan('│'));
141
+ console.log(chalk.cyan('│') + chalk.green(' ❯ 1. ') + chalk.white('Yes') + ' '.repeat(width - 13) + chalk.cyan('│'));
142
+ console.log(chalk.cyan('│') + ' ' + chalk.dim('2. ') + chalk.white('No') + ' '.repeat(width - 12) + chalk.cyan('│'));
143
+ console.log(chalk.cyan('╰' + '─'.repeat(width - 2) + '╯'));
144
+ console.log('');
145
+
146
+ const result = await this.waitForChoice(2);
147
+ return result.choice === 1;
148
+ }
149
+
150
+ /**
151
+ * Store preference to not ask again
152
+ */
153
+ setPreference(key, value) {
154
+ this.preferences.set(key, value);
155
+ }
156
+
157
+ /**
158
+ * Check if we should ask based on preferences
159
+ */
160
+ shouldAsk(key) {
161
+ return !this.preferences.has(key);
162
+ }
163
+
164
+ /**
165
+ * Clear all preferences
166
+ */
167
+ clearPreferences() {
168
+ this.preferences.clear();
169
+ }
170
+
171
+ /**
172
+ * Truncate text to fit width
173
+ */
174
+ truncate(text, maxLength) {
175
+ // Remove ANSI codes for length calculation
176
+ const plainText = text.replace(/\x1b\[[0-9;]*m/g, '');
177
+ if (plainText.length <= maxLength) return text;
178
+
179
+ // Find position in original text that corresponds to maxLength in plain text
180
+ let plainPos = 0;
181
+ let textPos = 0;
182
+ while (plainPos < maxLength - 3 && textPos < text.length) {
183
+ if (text.substr(textPos, 2) === '\x1b[') {
184
+ // Skip ANSI code
185
+ const endPos = text.indexOf('m', textPos);
186
+ if (endPos > -1) {
187
+ textPos = endPos + 1;
188
+ } else {
189
+ textPos++;
190
+ }
191
+ } else {
192
+ plainPos++;
193
+ textPos++;
194
+ }
195
+ }
196
+
197
+ return text.substring(0, textPos) + '...';
198
+ }
199
+
200
+ /**
201
+ * Format diff for display in dialog
202
+ */
203
+ formatDiffForDialog(oldContent, newContent) {
204
+ const oldLines = oldContent.split('\n');
205
+ const newLines = newContent.split('\n');
206
+ const maxLines = Math.max(oldLines.length, newLines.length);
207
+
208
+ let formatted = [];
209
+ for (let i = 0; i < Math.min(maxLines, 8); i++) {
210
+ const oldLine = oldLines[i];
211
+ const newLine = newLines[i];
212
+ const lineNum = String(i + 1).padStart(3, ' ');
213
+
214
+ if (oldLine !== newLine) {
215
+ if (oldLine !== undefined) {
216
+ formatted.push(chalk.red(`-${lineNum} ${oldLine}`));
217
+ }
218
+ if (newLine !== undefined) {
219
+ formatted.push(chalk.green(`+${lineNum} ${newLine}`));
220
+ }
221
+ } else {
222
+ formatted.push(chalk.dim(` ${lineNum} ${oldLine || ''}`));
223
+ }
224
+ }
225
+
226
+ return formatted.join('\n');
227
+ }
228
+ }
229
+
230
+ module.exports = ApprovalDialog;
@@ -32,9 +32,9 @@ class CollaborationManager {
32
32
  try {
33
33
  // Load existing team workspaces
34
34
  await this.loadTeamWorkspaces();
35
- console.log(chalk.blue('🤝 Collaboration manager initialized'));
35
+ // Silent initialization - no console output
36
36
  } catch (error) {
37
- console.log(chalk.yellow(`⚠️ Collaboration init warning: ${error.message}`));
37
+ // Silent error handling
38
38
  }
39
39
  }
40
40
 
@@ -0,0 +1,272 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ // Handle chalk properly
4
+ const chalkModule = require('chalk');
5
+ const chalk = chalkModule.default || chalkModule;
6
+
7
+ /**
8
+ * Diff Generator for Recoder Code
9
+ * Generates visual diffs for file operations
10
+ * Similar to Claude Code and OpenCode diff displays
11
+ */
12
+ class DiffGenerator {
13
+ constructor() {
14
+ this.maxDiffLines = 200; // Recommended by Claude Code
15
+
16
+ // File type icons mapping
17
+ this.fileIcons = {
18
+ '.js': '📜',
19
+ '.ts': '📘',
20
+ '.jsx': '⚛️',
21
+ '.tsx': '⚛️',
22
+ '.py': '🐍',
23
+ '.rb': '💎',
24
+ '.go': '🔷',
25
+ '.rs': '🦀',
26
+ '.java': '☕',
27
+ '.cpp': '⚙️',
28
+ '.c': '⚙️',
29
+ '.php': '🐘',
30
+ '.html': '🌐',
31
+ '.css': '🎨',
32
+ '.scss': '🎨',
33
+ '.json': '📋',
34
+ '.md': '📝',
35
+ '.yaml': '⚙️',
36
+ '.yml': '⚙️',
37
+ '.xml': '📄',
38
+ '.sh': '🔧',
39
+ '.sql': '🗃️',
40
+ '.txt': '📄',
41
+ '.log': '📊'
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get icon for file type
47
+ */
48
+ getFileIcon(filePath) {
49
+ const ext = path.extname(filePath).toLowerCase();
50
+ return this.fileIcons[ext] || '📄';
51
+ }
52
+
53
+ /**
54
+ * Generate diff for any file operation
55
+ * @param {string} operation - 'create', 'str_replace', 'view'
56
+ * @param {string} filePath
57
+ * @param {string} oldContent
58
+ * @param {string} newContent
59
+ * @returns {string} - Formatted diff
60
+ */
61
+ generateDiff(operation, filePath, oldContent = '', newContent = '') {
62
+ if (operation === 'create') {
63
+ return this.generateCreateDiff(filePath, newContent);
64
+ } else if (operation === 'str_replace') {
65
+ return this.generateEditDiff(filePath, oldContent, newContent);
66
+ } else if (operation === 'view') {
67
+ return this.generateViewDiff(filePath, oldContent);
68
+ }
69
+ return '';
70
+ }
71
+
72
+ /**
73
+ * Generate diff for file creation
74
+ */
75
+ generateCreateDiff(filePath, content) {
76
+ const lines = content.split('\n');
77
+ const size = Buffer.byteLength(content, 'utf8');
78
+ const fileName = path.basename(filePath);
79
+ const ext = path.extname(filePath);
80
+ const icon = this.getFileIcon(filePath);
81
+
82
+ // Build header
83
+ let diff = '\n';
84
+ diff += chalk.cyan('┌' + '─'.repeat(58) + '┐\n');
85
+ diff += chalk.cyan('│ ') + chalk.bold.green(`${icon} Create: `) + chalk.bold(fileName);
86
+ diff += ' '.repeat(Math.max(0, 58 - 14 - fileName.length)) + chalk.cyan('│\n');
87
+ diff += chalk.cyan('│ ') + chalk.dim(`Size: ${this.formatBytes(size)} | Lines: ${lines.length} | Type: ${ext || 'txt'}`);
88
+ diff += ' '.repeat(Math.max(0, 58 - 35 - ext.length - size.toString().length - lines.length.toString().length)) + chalk.cyan('│\n');
89
+ diff += chalk.cyan('├' + '─'.repeat(58) + '┤\n');
90
+
91
+ // Show content with line numbers (limit to max lines)
92
+ const displayLines = lines.slice(0, this.maxDiffLines);
93
+ const lineNumWidth = displayLines.length.toString().length;
94
+
95
+ displayLines.forEach((line, i) => {
96
+ const lineNum = (i + 1).toString().padStart(lineNumWidth, ' ');
97
+ const displayLine = this.truncateLine(line, 52);
98
+ diff += chalk.cyan('│ ') + chalk.green(`+ ${lineNum} │ ${displayLine}`);
99
+ diff += ' '.repeat(Math.max(0, 58 - 6 - lineNumWidth - displayLine.length)) + chalk.cyan('│\n');
100
+ });
101
+
102
+ if (lines.length > this.maxDiffLines) {
103
+ diff += chalk.cyan('│ ') + chalk.dim(`... ${lines.length - this.maxDiffLines} more lines`);
104
+ diff += ' '.repeat(Math.max(0, 58 - 15 - (lines.length - this.maxDiffLines).toString().length)) + chalk.cyan('│\n');
105
+ }
106
+
107
+ // Footer
108
+ diff += chalk.cyan('├' + '─'.repeat(58) + '┤\n');
109
+ diff += chalk.cyan('│ ') + chalk.green(`+${lines.length} lines to add`);
110
+ diff += ' '.repeat(Math.max(0, 58 - 17 - lines.length.toString().length)) + chalk.cyan('│\n');
111
+ diff += chalk.cyan('└' + '─'.repeat(58) + '┘\n');
112
+
113
+ return diff;
114
+ }
115
+
116
+ /**
117
+ * Generate diff for file editing
118
+ */
119
+ generateEditDiff(filePath, oldContent, newContent) {
120
+ const oldLines = oldContent.split('\n');
121
+ const newLines = newContent.split('\n');
122
+ const fileName = path.basename(filePath);
123
+ const icon = this.getFileIcon(filePath);
124
+
125
+ // Calculate stats
126
+ const oldSize = Buffer.byteLength(oldContent, 'utf8');
127
+ const newSize = Buffer.byteLength(newContent, 'utf8');
128
+ const sizeDiff = newSize - oldSize;
129
+ const lineDiff = newLines.length - oldLines.length;
130
+
131
+ // Build header
132
+ let diff = '\n';
133
+ diff += chalk.cyan('┌' + '─'.repeat(58) + '┐\n');
134
+ diff += chalk.cyan('│ ') + chalk.bold.yellow(`${icon} Edit: `) + chalk.bold(fileName);
135
+ diff += ' '.repeat(Math.max(0, 58 - 13 - fileName.length)) + chalk.cyan('│\n');
136
+ diff += chalk.cyan('│ ') + chalk.dim(`Lines: ${oldLines.length} → ${newLines.length} (${lineDiff >= 0 ? '+' : ''}${lineDiff})`);
137
+ diff += ' '.repeat(Math.max(0, 58 - 20 - oldLines.length.toString().length - newLines.length.toString().length - lineDiff.toString().length)) + chalk.cyan('│\n');
138
+ diff += chalk.cyan('├' + '─'.repeat(58) + '┤\n');
139
+
140
+ // Generate unified diff
141
+ const diffLines = this.generateUnifiedDiff(oldLines, newLines);
142
+ const displayLines = diffLines.slice(0, this.maxDiffLines);
143
+
144
+ displayLines.forEach(({ type, lineNum, content }) => {
145
+ const displayLine = this.truncateLine(content, 48);
146
+ const num = lineNum.toString().padStart(3, ' ');
147
+
148
+ if (type === 'add') {
149
+ diff += chalk.cyan('│ ') + chalk.green(`+ ${num} │ ${displayLine}`);
150
+ diff += ' '.repeat(Math.max(0, 58 - 8 - displayLine.length)) + chalk.cyan('│\n');
151
+ } else if (type === 'remove') {
152
+ diff += chalk.cyan('│ ') + chalk.red(`- ${num} │ ${displayLine}`);
153
+ diff += ' '.repeat(Math.max(0, 58 - 8 - displayLine.length)) + chalk.cyan('│\n');
154
+ } else {
155
+ diff += chalk.cyan('│ ') + chalk.dim(` ${num} │ ${displayLine}`);
156
+ diff += ' '.repeat(Math.max(0, 58 - 8 - displayLine.length)) + chalk.cyan('│\n');
157
+ }
158
+ });
159
+
160
+ if (diffLines.length > this.maxDiffLines) {
161
+ diff += chalk.cyan('│ ') + chalk.dim(`... ${diffLines.length - this.maxDiffLines} more lines`);
162
+ diff += ' '.repeat(Math.max(0, 58 - 15 - (diffLines.length - this.maxDiffLines).toString().length)) + chalk.cyan('│\n');
163
+ }
164
+
165
+ // Footer with stats
166
+ diff += chalk.cyan('├' + '─'.repeat(58) + '┤\n');
167
+ const added = diffLines.filter(l => l.type === 'add').length;
168
+ const removed = diffLines.filter(l => l.type === 'remove').length;
169
+ diff += chalk.cyan('│ ') + chalk.dim(`Changes: `) + chalk.green(`+${added}`) + chalk.dim(' ') + chalk.red(`-${removed}`);
170
+ diff += ' '.repeat(Math.max(0, 58 - 14 - added.toString().length - removed.toString().length)) + chalk.cyan('│\n');
171
+ diff += chalk.cyan('└' + '─'.repeat(58) + '┘\n');
172
+
173
+ return diff;
174
+ }
175
+
176
+ /**
177
+ * Generate view-only diff (for file preview)
178
+ */
179
+ generateViewDiff(filePath, content) {
180
+ const lines = content.split('\n');
181
+ const size = Buffer.byteLength(content, 'utf8');
182
+ const fileName = path.basename(filePath);
183
+ const icon = this.getFileIcon(filePath);
184
+
185
+ let diff = '\n';
186
+ diff += chalk.cyan('┌' + '─'.repeat(58) + '┐\n');
187
+ diff += chalk.cyan('│ ') + chalk.bold.blue(`${icon} View: `) + chalk.bold(fileName);
188
+ diff += ' '.repeat(Math.max(0, 58 - 13 - fileName.length)) + chalk.cyan('│\n');
189
+ diff += chalk.cyan('│ ') + chalk.dim(`Size: ${this.formatBytes(size)} | Lines: ${lines.length}`);
190
+ diff += ' '.repeat(Math.max(0, 58 - 25 - size.toString().length - lines.length.toString().length)) + chalk.cyan('│\n');
191
+ diff += chalk.cyan('├' + '─'.repeat(58) + '┤\n');
192
+
193
+ const displayLines = lines.slice(0, this.maxDiffLines);
194
+ const lineNumWidth = displayLines.length.toString().length;
195
+
196
+ displayLines.forEach((line, i) => {
197
+ const lineNum = (i + 1).toString().padStart(lineNumWidth, ' ');
198
+ const displayLine = this.truncateLine(line, 52 - lineNumWidth);
199
+ diff += chalk.cyan('│ ') + chalk.dim(`${lineNum} │ ${displayLine}`);
200
+ diff += ' '.repeat(Math.max(0, 58 - 4 - lineNumWidth - displayLine.length)) + chalk.cyan('│\n');
201
+ });
202
+
203
+ if (lines.length > this.maxDiffLines) {
204
+ diff += chalk.cyan('│ ') + chalk.dim(`... ${lines.length - this.maxDiffLines} more lines`);
205
+ diff += ' '.repeat(Math.max(0, 58 - 15 - (lines.length - this.maxDiffLines).toString().length)) + chalk.cyan('│\n');
206
+ }
207
+
208
+ diff += chalk.cyan('└' + '─'.repeat(58) + '┘\n');
209
+
210
+ return diff;
211
+ }
212
+
213
+ /**
214
+ * Generate unified diff from two arrays of lines
215
+ * Returns array of { type, lineNum, content }
216
+ */
217
+ generateUnifiedDiff(oldLines, newLines) {
218
+ const diff = [];
219
+ const maxLines = Math.max(oldLines.length, newLines.length);
220
+
221
+ // Simple line-by-line diff (can be enhanced with better algorithm)
222
+ let oldIdx = 0;
223
+ let newIdx = 0;
224
+
225
+ while (oldIdx < oldLines.length || newIdx < newLines.length) {
226
+ const oldLine = oldLines[oldIdx];
227
+ const newLine = newLines[newIdx];
228
+
229
+ if (oldLine === newLine) {
230
+ // Unchanged line
231
+ diff.push({ type: 'context', lineNum: newIdx + 1, content: newLine });
232
+ oldIdx++;
233
+ newIdx++;
234
+ } else if (oldIdx >= oldLines.length) {
235
+ // Only new lines left
236
+ diff.push({ type: 'add', lineNum: newIdx + 1, content: newLine });
237
+ newIdx++;
238
+ } else if (newIdx >= newLines.length) {
239
+ // Only old lines left
240
+ diff.push({ type: 'remove', lineNum: oldIdx + 1, content: oldLine });
241
+ oldIdx++;
242
+ } else {
243
+ // Lines differ - simple approach: mark as remove + add
244
+ diff.push({ type: 'remove', lineNum: oldIdx + 1, content: oldLine });
245
+ diff.push({ type: 'add', lineNum: newIdx + 1, content: newLine });
246
+ oldIdx++;
247
+ newIdx++;
248
+ }
249
+ }
250
+
251
+ return diff;
252
+ }
253
+
254
+ /**
255
+ * Format bytes to human-readable size
256
+ */
257
+ formatBytes(bytes) {
258
+ if (bytes < 1024) return bytes + 'B';
259
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
260
+ return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
261
+ }
262
+
263
+ /**
264
+ * Truncate line to max width
265
+ */
266
+ truncateLine(line, maxWidth) {
267
+ if (line.length <= maxWidth) return line;
268
+ return line.substring(0, maxWidth - 3) + '...';
269
+ }
270
+ }
271
+
272
+ module.exports = DiffGenerator;
@@ -0,0 +1,265 @@
1
+ const chalkModule = require('chalk');
2
+ const chalk = chalkModule.default || chalkModule;
3
+
4
+ /**
5
+ * Output Formatter for Claude Code-style display
6
+ * Provides collapsible sections, progress indicators, and clean formatting
7
+ */
8
+ class OutputFormatter {
9
+ constructor() {
10
+ this.collapsedSections = new Map();
11
+ this.truncateLength = 10; // Show first N lines by default
12
+ }
13
+
14
+ /**
15
+ * Format tool execution header (like Claude Code's ⏺ Bash(...))
16
+ */
17
+ formatToolHeader(toolName, args) {
18
+ let header = chalk.dim('⏺ ');
19
+
20
+ switch (toolName) {
21
+ case 'bash':
22
+ header += chalk.cyan(`Bash(${this.truncateArg(args.command)})`);
23
+ break;
24
+ case 'str_replace_editor':
25
+ header += chalk.cyan(`Edit(${args.command} ${this.truncateArg(args.path)})`);
26
+ break;
27
+ case 'codebase_search':
28
+ header += chalk.cyan(`Search(${this.truncateArg(args.query)})`);
29
+ break;
30
+ case 'web_fetch':
31
+ header += chalk.cyan(`WebFetch(${this.truncateArg(args.url)})`);
32
+ break;
33
+ default:
34
+ header += chalk.cyan(`${toolName}(...)`);
35
+ }
36
+
37
+ return header;
38
+ }
39
+
40
+ /**
41
+ * Format tool execution body with collapsible output
42
+ */
43
+ formatToolBody(output, sectionId, truncate = true) {
44
+ if (!output) return '';
45
+
46
+ const lines = output.split('\n');
47
+ const totalLines = lines.length;
48
+
49
+ if (!truncate || totalLines <= this.truncateLength) {
50
+ // Show all lines with indent
51
+ return lines.map(line => chalk.dim(' ⎿ ') + line).join('\n');
52
+ }
53
+
54
+ // Show truncated with expand option
55
+ const visibleLines = lines.slice(0, this.truncateLength);
56
+ const hiddenCount = totalLines - this.truncateLength;
57
+
58
+ let formatted = visibleLines.map(line => chalk.dim(' ⎿ ') + line).join('\n');
59
+ formatted += '\n' + chalk.dim(' ⎿ ') + chalk.yellow(`… +${hiddenCount} lines `) + chalk.dim('(ctrl+o to expand)');
60
+
61
+ // Store full output for expansion
62
+ this.collapsedSections.set(sectionId, lines);
63
+
64
+ return formatted;
65
+ }
66
+
67
+ /**
68
+ * Format thinking/processing indicator
69
+ */
70
+ formatThinking(message = 'Thinking') {
71
+ return chalk.dim('✳ ') + chalk.cyan(message) + chalk.dim('…');
72
+ }
73
+
74
+ /**
75
+ * Format status line (bottom bar like Claude Code)
76
+ */
77
+ formatStatusBar(mode, sessionInfo = {}) {
78
+ const modeIcons = {
79
+ ask: '⏸',
80
+ auto: '⏵⏵',
81
+ plan: '📖',
82
+ build: '🔨'
83
+ };
84
+
85
+ const modeIcon = modeIcons[mode] || '⏸';
86
+ const modeText = mode === 'auto' ? 'accept edits on' : mode + ' mode';
87
+
88
+ let status = chalk.dim(' ') + modeIcon + chalk.dim(` ${modeText} `);
89
+ status += chalk.dim('(shift+tab to cycle)');
90
+
91
+ // Add background tasks if any
92
+ if (sessionInfo.backgroundTasks > 0) {
93
+ status += chalk.dim(` · ${sessionInfo.backgroundTasks} background task${sessionInfo.backgroundTasks > 1 ? 's' : ''}`);
94
+ }
95
+
96
+ // Add shortcuts hint
97
+ status += chalk.dim(' · ? for shortcuts');
98
+
99
+ // Add context info
100
+ if (sessionInfo.contextUsage) {
101
+ status += chalk.dim(` Context left: ${sessionInfo.contextUsage}%`);
102
+ }
103
+
104
+ return status;
105
+ }
106
+
107
+ /**
108
+ * Format separator line
109
+ */
110
+ formatSeparator(width = 90) {
111
+ return chalk.dim('─'.repeat(width));
112
+ }
113
+
114
+ /**
115
+ * Format success message (like Claude Code's ⏺ Perfect! ...)
116
+ */
117
+ formatSuccess(message) {
118
+ return chalk.dim('⏺ ') + chalk.green(message);
119
+ }
120
+
121
+ /**
122
+ * Format error message
123
+ */
124
+ formatError(message) {
125
+ return chalk.dim('⏺ ') + chalk.red('Error: ') + message;
126
+ }
127
+
128
+ /**
129
+ * Format info message
130
+ */
131
+ formatInfo(message) {
132
+ return chalk.dim('⏺ ') + chalk.cyan(message);
133
+ }
134
+
135
+ /**
136
+ * Truncate argument for display
137
+ */
138
+ truncateArg(arg, maxLength = 50) {
139
+ if (!arg) return '';
140
+ const str = String(arg);
141
+ if (str.length <= maxLength) return str;
142
+ return str.substring(0, maxLength - 3) + '...';
143
+ }
144
+
145
+ /**
146
+ * Format multi-line output with proper indentation
147
+ */
148
+ formatIndented(text, indent = ' ') {
149
+ const lines = text.split('\n');
150
+ return lines.map(line => chalk.dim(indent + '⎿ ') + line).join('\n');
151
+ }
152
+
153
+ /**
154
+ * Format code block
155
+ */
156
+ formatCodeBlock(code, language = '') {
157
+ const lines = code.split('\n');
158
+ return lines.map((line, i) => {
159
+ const lineNum = String(i + 1).padStart(3, ' ');
160
+ return chalk.dim(` ${lineNum} `) + chalk.gray('│ ') + line;
161
+ }).join('\n');
162
+ }
163
+
164
+ /**
165
+ * Expand collapsed section
166
+ */
167
+ expandSection(sectionId) {
168
+ const lines = this.collapsedSections.get(sectionId);
169
+ if (!lines) return null;
170
+
171
+ return lines.map(line => chalk.dim(' ⎿ ') + line).join('\n');
172
+ }
173
+
174
+ /**
175
+ * Format progress with spinner
176
+ */
177
+ formatProgress(message, step = 0, total = 0) {
178
+ const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
179
+ const spinner = spinners[step % spinners.length];
180
+
181
+ let progress = chalk.cyan(spinner) + ' ' + message;
182
+
183
+ if (total > 0) {
184
+ progress += chalk.dim(` (${step}/${total})`);
185
+ }
186
+
187
+ return progress;
188
+ }
189
+
190
+ /**
191
+ * Format file tree structure
192
+ */
193
+ formatFileTree(files, depth = 0) {
194
+ const indent = ' '.repeat(depth);
195
+ return files.map(file => {
196
+ const icon = file.isDirectory ? '📁' : this.getFileIcon(file.name);
197
+ return chalk.dim(indent) + icon + ' ' + chalk.white(file.name);
198
+ }).join('\n');
199
+ }
200
+
201
+ /**
202
+ * Get file icon by extension
203
+ */
204
+ getFileIcon(filename) {
205
+ const ext = filename.split('.').pop();
206
+ const icons = {
207
+ 'js': '📜', 'ts': '📘', 'jsx': '⚛️', 'tsx': '⚛️',
208
+ 'py': '🐍', 'rb': '💎', 'go': '🔷', 'rs': '🦀',
209
+ 'java': '☕', 'cpp': '⚙️', 'c': '⚙️', 'php': '🐘',
210
+ 'html': '🌐', 'css': '🎨', 'json': '📋', 'md': '📝',
211
+ 'yaml': '⚙️', 'yml': '⚙️', 'xml': '📄', 'sh': '🔧',
212
+ 'sql': '🗃️', 'txt': '📄', 'log': '📊'
213
+ };
214
+ return icons[ext] || '📄';
215
+ }
216
+
217
+ /**
218
+ * Format list with bullets
219
+ */
220
+ formatList(items, bullet = '•') {
221
+ return items.map(item => chalk.dim(' ' + bullet + ' ') + item).join('\n');
222
+ }
223
+
224
+ /**
225
+ * Format numbered list
226
+ */
227
+ formatNumberedList(items) {
228
+ return items.map((item, i) => {
229
+ const num = String(i + 1).padStart(2, ' ');
230
+ return chalk.dim(` ${num}. `) + item;
231
+ }).join('\n');
232
+ }
233
+
234
+ /**
235
+ * Clear current line
236
+ */
237
+ clearLine() {
238
+ process.stdout.write('\r\x1b[K');
239
+ }
240
+
241
+ /**
242
+ * Move cursor up N lines
243
+ */
244
+ moveCursorUp(lines = 1) {
245
+ process.stdout.write(`\x1b[${lines}A`);
246
+ }
247
+
248
+ /**
249
+ * Format elapsed time
250
+ */
251
+ formatElapsedTime(startTime) {
252
+ const elapsed = Date.now() - startTime;
253
+ const seconds = Math.floor(elapsed / 1000);
254
+
255
+ if (seconds < 60) {
256
+ return `${seconds}s`;
257
+ }
258
+
259
+ const minutes = Math.floor(seconds / 60);
260
+ const remainingSeconds = seconds % 60;
261
+ return `${minutes}m ${remainingSeconds}s`;
262
+ }
263
+ }
264
+
265
+ module.exports = OutputFormatter;
@@ -0,0 +1,223 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * Permission Manager for Recoder Code
7
+ * Manages approval workflows for file operations and commands
8
+ * Similar to Claude Code and OpenCode permission systems
9
+ */
10
+ class PermissionManager {
11
+ constructor() {
12
+ this.configPath = path.join(os.homedir(), '.recoder-code', 'config.json');
13
+ this.config = this.loadConfig();
14
+ this.sessionAutoAccept = false;
15
+ this.sessionMode = null; // Override config mode for current session
16
+ }
17
+
18
+ /**
19
+ * Load configuration from ~/.recoder-code/config.json
20
+ */
21
+ loadConfig() {
22
+ try {
23
+ if (fs.existsSync(this.configPath)) {
24
+ const configData = fs.readFileSync(this.configPath, 'utf8');
25
+ const config = JSON.parse(configData);
26
+
27
+ // Ensure permission object exists
28
+ if (!config.permission) {
29
+ config.permission = this.getDefaultPermissions();
30
+ }
31
+
32
+ return config;
33
+ }
34
+ } catch (error) {
35
+ // Silent error - use defaults
36
+ }
37
+
38
+ // Default configuration
39
+ return {
40
+ permission: this.getDefaultPermissions()
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Get default permission settings
46
+ */
47
+ getDefaultPermissions() {
48
+ return {
49
+ mode: 'ask', // 'ask', 'auto', 'deny'
50
+ edit: 'ask', // File editing permission
51
+ bash: 'ask', // Bash command permission
52
+ autoAcceptAll: false
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Save configuration to ~/.recoder-code/config.json
58
+ */
59
+ saveConfig() {
60
+ try {
61
+ const configDir = path.dirname(this.configPath);
62
+ if (!fs.existsSync(configDir)) {
63
+ fs.mkdirSync(configDir, { recursive: true });
64
+ }
65
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
66
+ return true;
67
+ } catch (error) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Check if we should ask for permission for a specific operation
74
+ * @param {string} operation - 'edit', 'bash', 'web_fetch', etc.
75
+ * @returns {boolean} - true if should ask, false if auto-approve
76
+ */
77
+ shouldAskPermission(operation) {
78
+ // Ensure config and permission object exist
79
+ if (!this.config || !this.config.permission) {
80
+ this.config = { permission: this.getDefaultPermissions() };
81
+ }
82
+
83
+ // Session auto-accept overrides everything
84
+ if (this.sessionAutoAccept) {
85
+ return false;
86
+ }
87
+
88
+ // Session mode overrides config
89
+ const mode = this.sessionMode || this.config.permission.mode || 'ask';
90
+
91
+ // Check global mode first
92
+ if (mode === 'auto') {
93
+ return false;
94
+ }
95
+
96
+ if (mode === 'deny') {
97
+ return true; // Will be denied, but still "ask"
98
+ }
99
+
100
+ // Check operation-specific permission
101
+ const operationPerm = this.config.permission[operation] || 'ask';
102
+ if (operationPerm === 'allow') {
103
+ return false;
104
+ }
105
+
106
+ if (operationPerm === 'deny') {
107
+ return true;
108
+ }
109
+
110
+ // Default to ask
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Check if an operation is denied
116
+ * @param {string} operation
117
+ * @returns {boolean}
118
+ */
119
+ isDenied(operation) {
120
+ // Ensure config and permission object exist
121
+ if (!this.config || !this.config.permission) {
122
+ this.config = { permission: this.getDefaultPermissions() };
123
+ }
124
+
125
+ const mode = this.sessionMode || this.config.permission.mode || 'ask';
126
+
127
+ if (mode === 'deny') {
128
+ return true;
129
+ }
130
+
131
+ const operationPerm = this.config.permission[operation] || 'ask';
132
+ return operationPerm === 'deny';
133
+ }
134
+
135
+ /**
136
+ * Enable auto-accept for the current session
137
+ * Similar to Claude Code's "⏵⏵ accept edits on"
138
+ */
139
+ setAutoAcceptSession(enabled) {
140
+ this.sessionAutoAccept = enabled;
141
+ }
142
+
143
+ /**
144
+ * Get current auto-accept status
145
+ */
146
+ isAutoAcceptEnabled() {
147
+ return this.sessionAutoAccept;
148
+ }
149
+
150
+ /**
151
+ * Set session mode (overrides config)
152
+ * @param {string} mode - 'ask', 'auto', 'plan', 'deny'
153
+ */
154
+ setSessionMode(mode) {
155
+ this.sessionMode = mode;
156
+ }
157
+
158
+ /**
159
+ * Get current mode (session override or config)
160
+ */
161
+ getCurrentMode() {
162
+ // Ensure config and permission object exist
163
+ if (!this.config || !this.config.permission) {
164
+ this.config = { permission: this.getDefaultPermissions() };
165
+ }
166
+ return this.sessionMode || this.config.permission.mode || 'ask';
167
+ }
168
+
169
+ /**
170
+ * Update config permission for an operation
171
+ * @param {string} operation
172
+ * @param {string} value - 'ask', 'allow', 'deny'
173
+ */
174
+ setPermission(operation, value) {
175
+ // Ensure config and permission object exist
176
+ if (!this.config || !this.config.permission) {
177
+ this.config = { permission: this.getDefaultPermissions() };
178
+ }
179
+ this.config.permission[operation] = value;
180
+ this.saveConfig();
181
+ }
182
+
183
+ /**
184
+ * Update global mode
185
+ * @param {string} mode - 'ask', 'auto', 'deny'
186
+ */
187
+ setMode(mode) {
188
+ // Ensure config and permission object exist
189
+ if (!this.config || !this.config.permission) {
190
+ this.config = { permission: this.getDefaultPermissions() };
191
+ }
192
+ this.config.permission.mode = mode;
193
+ this.saveConfig();
194
+ }
195
+
196
+ /**
197
+ * Reset to defaults
198
+ */
199
+ resetToDefaults() {
200
+ this.config = {
201
+ permission: this.getDefaultPermissions()
202
+ };
203
+ this.sessionAutoAccept = false;
204
+ this.sessionMode = null;
205
+ this.saveConfig();
206
+ }
207
+
208
+ /**
209
+ * Get human-readable mode description
210
+ */
211
+ getModeDescription() {
212
+ const mode = this.getCurrentMode();
213
+ const descriptions = {
214
+ ask: '⏸ Manual approval mode - will ask for each change',
215
+ auto: '⏵⏵ Auto-accept mode - all changes approved automatically',
216
+ plan: '📖 Plan mode - read-only, no changes allowed',
217
+ deny: '⊘ Deny mode - all changes blocked'
218
+ };
219
+ return descriptions[mode] || descriptions.ask;
220
+ }
221
+ }
222
+
223
+ module.exports = PermissionManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recoder-code",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "description": "🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!",
5
5
  "main": "index.js",
6
6
  "scripts": {