banana-code 1.2.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -0
  3. package/banana.js +5464 -0
  4. package/lib/agenticRunner.js +1884 -0
  5. package/lib/borderRenderer.js +41 -0
  6. package/lib/commandRunner.js +205 -0
  7. package/lib/completer.js +286 -0
  8. package/lib/config.js +301 -0
  9. package/lib/contextBuilder.js +324 -0
  10. package/lib/diffViewer.js +295 -0
  11. package/lib/fileManager.js +224 -0
  12. package/lib/historyManager.js +124 -0
  13. package/lib/hookManager.js +1143 -0
  14. package/lib/imageHandler.js +268 -0
  15. package/lib/inlineComplete.js +192 -0
  16. package/lib/interactivePicker.js +254 -0
  17. package/lib/lmStudio.js +226 -0
  18. package/lib/markdownRenderer.js +423 -0
  19. package/lib/mcpClient.js +288 -0
  20. package/lib/modelRegistry.js +350 -0
  21. package/lib/monkeyModels.js +97 -0
  22. package/lib/oauthOpenAI.js +167 -0
  23. package/lib/parser.js +134 -0
  24. package/lib/promptManager.js +96 -0
  25. package/lib/providerClients.js +1014 -0
  26. package/lib/providerManager.js +130 -0
  27. package/lib/providerStore.js +413 -0
  28. package/lib/statusBar.js +283 -0
  29. package/lib/streamHandler.js +306 -0
  30. package/lib/subAgentManager.js +406 -0
  31. package/lib/tokenCounter.js +132 -0
  32. package/lib/visionAnalyzer.js +163 -0
  33. package/lib/watcher.js +138 -0
  34. package/models.json +57 -0
  35. package/package.json +42 -0
  36. package/prompts/base.md +23 -0
  37. package/prompts/code-agent-glm.md +16 -0
  38. package/prompts/code-agent-gptoss.md +25 -0
  39. package/prompts/code-agent-nemotron.md +17 -0
  40. package/prompts/code-agent-qwen.md +20 -0
  41. package/prompts/code-agent.md +70 -0
  42. package/prompts/plan.md +44 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Diff Viewer - Show colorful diffs for file changes
3
+ */
4
+
5
+ const { diffLines, createPatch } = require('diff');
6
+
7
+ // ANSI colors (fallback if chalk not available)
8
+ const colors = {
9
+ reset: '\x1b[0m',
10
+ dim: '\x1b[2m',
11
+ green: '\x1b[38;5;120m',
12
+ red: '\x1b[38;5;210m',
13
+ cyan: '\x1b[38;5;51m',
14
+ yellow: '\x1b[38;5;226m',
15
+ banana: '\x1b[38;5;220m',
16
+ bananaGlow: '\x1b[38;5;228m',
17
+ orange: '\x1b[38;5;220m',
18
+ gray: '\x1b[38;5;245m',
19
+ white: '\x1b[38;5;255m',
20
+ magenta: '\x1b[38;5;183m',
21
+ bgGreen: '\x1b[48;5;22m',
22
+ bgRed: '\x1b[48;5;52m'
23
+ };
24
+
25
+ /**
26
+ * Format a line number with padding
27
+ */
28
+ function formatLineNum(num, width = 4) {
29
+ return String(num).padStart(width, ' ');
30
+ }
31
+
32
+ /**
33
+ * Generate a unified diff view
34
+ */
35
+ function generateDiff(oldContent, newContent, filePath) {
36
+ const patch = createPatch(filePath, oldContent || '', newContent, 'old', 'new');
37
+ return patch;
38
+ }
39
+
40
+ /**
41
+ * Create a pretty diff display for terminal
42
+ */
43
+ function prettyDiff(oldContent, newContent, filePath, maxLines = 50) {
44
+ const differences = diffLines(oldContent || '', newContent);
45
+ const lines = [];
46
+ let lineCount = 0;
47
+ let hasChanges = false;
48
+
49
+ // Header
50
+ lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
51
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.white}${filePath}${colors.reset}`);
52
+ lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
53
+
54
+ let oldLineNum = 1;
55
+ let newLineNum = 1;
56
+
57
+ for (const part of differences) {
58
+ const partLines = part.value.split('\n');
59
+ // Remove empty last line from split
60
+ if (partLines[partLines.length - 1] === '') {
61
+ partLines.pop();
62
+ }
63
+
64
+ for (const line of partLines) {
65
+ if (lineCount >= maxLines) {
66
+ lines.push(`${colors.gray} ... ${differences.length - lineCount} more changes ...${colors.reset}`);
67
+ break;
68
+ }
69
+
70
+ if (part.added) {
71
+ hasChanges = true;
72
+ lines.push(`${colors.green}+ ${formatLineNum(newLineNum)}${colors.reset} ${colors.bgGreen}${line}${colors.reset}`);
73
+ newLineNum++;
74
+ } else if (part.removed) {
75
+ hasChanges = true;
76
+ lines.push(`${colors.red}- ${formatLineNum(oldLineNum)}${colors.reset} ${colors.bgRed}${line}${colors.reset}`);
77
+ oldLineNum++;
78
+ } else {
79
+ // Context line (unchanged) - show less of these
80
+ if (lineCount < 5 || lineCount > differences.length - 5) {
81
+ lines.push(`${colors.gray} ${formatLineNum(oldLineNum)}${colors.reset} ${line}`);
82
+ } else if (lines[lines.length - 1] !== '...') {
83
+ lines.push(`${colors.gray} ...${colors.reset}`);
84
+ }
85
+ oldLineNum++;
86
+ newLineNum++;
87
+ }
88
+
89
+ lineCount++;
90
+ }
91
+
92
+ if (lineCount >= maxLines) break;
93
+ }
94
+
95
+ if (!hasChanges) {
96
+ lines.push(`${colors.gray} (no changes)${colors.reset}`);
97
+ }
98
+
99
+ lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
100
+
101
+ return lines.join('\n');
102
+ }
103
+
104
+ /**
105
+ * Show a new file creation preview
106
+ */
107
+ function showNewFile(content, filePath, maxLines = 30) {
108
+ const lines = content.split('\n');
109
+ const output = [];
110
+
111
+ output.push(`${colors.green}CREATE${colors.reset} ${colors.white}${filePath}${colors.reset}`);
112
+ output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
113
+
114
+ for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
115
+ output.push(`${colors.green}+ ${formatLineNum(i + 1)}${colors.reset} ${lines[i]}`);
116
+ }
117
+
118
+ if (lines.length > maxLines) {
119
+ output.push(`${colors.gray} ... ${lines.length - maxLines} more lines ...${colors.reset}`);
120
+ }
121
+
122
+ output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
123
+ output.push(`${colors.dim}${lines.length} lines${colors.reset}`);
124
+
125
+ return output.join('\n');
126
+ }
127
+
128
+ /**
129
+ * Show a file deletion preview
130
+ */
131
+ function showDeleteFile(content, filePath, maxLines = 15) {
132
+ const lines = content ? content.split('\n') : [];
133
+ const output = [];
134
+
135
+ output.push(`${colors.red}DELETE${colors.reset} ${colors.white}${filePath}${colors.reset}`);
136
+ output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
137
+
138
+ for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
139
+ output.push(`${colors.red}- ${formatLineNum(i + 1)}${colors.reset} ${colors.dim}${lines[i]}${colors.reset}`);
140
+ }
141
+
142
+ if (lines.length > maxLines) {
143
+ output.push(`${colors.gray} ... ${lines.length - maxLines} more lines ...${colors.reset}`);
144
+ }
145
+
146
+ output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
147
+
148
+ return output.join('\n');
149
+ }
150
+
151
+ /**
152
+ * Show an edit preview
153
+ */
154
+ function showEdit(oldContent, newContent, filePath) {
155
+ return prettyDiff(oldContent, newContent, filePath);
156
+ }
157
+
158
+ /**
159
+ * Get change summary (for compact display)
160
+ */
161
+ function getChangeSummary(oldContent, newContent) {
162
+ const differences = diffLines(oldContent || '', newContent);
163
+ let added = 0;
164
+ let removed = 0;
165
+
166
+ for (const part of differences) {
167
+ const lineCount = part.value.split('\n').length - 1;
168
+ if (part.added) added += lineCount;
169
+ if (part.removed) removed += lineCount;
170
+ }
171
+
172
+ return { added, removed };
173
+ }
174
+
175
+ /**
176
+ * Format operation for display
177
+ */
178
+ function formatOperation(operation, existingContent = null) {
179
+ const { action, path, content } = operation;
180
+
181
+ switch (action) {
182
+ case 'create':
183
+ return showNewFile(content, path);
184
+
185
+ case 'delete':
186
+ return showDeleteFile(existingContent, path);
187
+
188
+ case 'edit':
189
+ if (existingContent === null) {
190
+ // No existing content, treat as create
191
+ return showNewFile(content, path);
192
+ }
193
+ return showEdit(existingContent, content, path);
194
+
195
+ default:
196
+ return `Unknown action: ${action}`;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Format multiple operations in a batched view with summary
202
+ */
203
+ function formatOperationsBatch(operations, fileManager) {
204
+ const output = [];
205
+ const summary = { created: 0, modified: 0, deleted: 0, additions: 0, deletions: 0 };
206
+
207
+ // Header
208
+ output.push(`\n${colors.banana}${'═'.repeat(60)}${colors.reset}`);
209
+ output.push(`${colors.banana} PROPOSED CHANGES (${operations.length} files)${colors.reset}`);
210
+ output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}\n`);
211
+
212
+ // Process each operation
213
+ for (let i = 0; i < operations.length; i++) {
214
+ const op = operations[i];
215
+ const existingContent = fileManager.exists(op.path)
216
+ ? fileManager.readFile(op.path).content
217
+ : null;
218
+
219
+ // Get change stats
220
+ if (op.action === 'create') {
221
+ summary.created++;
222
+ const lines = op.content.split('\n').length;
223
+ summary.additions += lines;
224
+ } else if (op.action === 'delete') {
225
+ summary.deleted++;
226
+ if (existingContent) {
227
+ summary.deletions += existingContent.split('\n').length;
228
+ }
229
+ } else if (op.action === 'edit') {
230
+ summary.modified++;
231
+ const changes = getChangeSummary(existingContent, op.content);
232
+ summary.additions += changes.added;
233
+ summary.deletions += changes.removed;
234
+ }
235
+
236
+ // File number indicator
237
+ output.push(`${colors.dim}[${i + 1}/${operations.length}]${colors.reset}`);
238
+ output.push(formatOperation(op, existingContent));
239
+ output.push('');
240
+ }
241
+
242
+ // Summary footer
243
+ output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}`);
244
+
245
+ const parts = [];
246
+ if (summary.created > 0) parts.push(`${colors.green}+${summary.created} new${colors.reset}`);
247
+ if (summary.modified > 0) parts.push(`${colors.yellow}~${summary.modified} modified${colors.reset}`);
248
+ if (summary.deleted > 0) parts.push(`${colors.red}-${summary.deleted} deleted${colors.reset}`);
249
+
250
+ output.push(`${colors.white} Summary:${colors.reset} ${parts.join(' | ')}`);
251
+ output.push(`${colors.dim} Lines: ${colors.green}+${summary.additions}${colors.reset} ${colors.red}-${summary.deletions}${colors.reset}`);
252
+ output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}\n`);
253
+
254
+ return output.join('\n');
255
+ }
256
+
257
+ /**
258
+ * Show a compact summary of changes (for confirmation prompts)
259
+ */
260
+ function formatCompactSummary(operations, fileManager) {
261
+ const lines = [`\n${colors.cyan}Changes:${colors.reset}`];
262
+
263
+ for (const op of operations) {
264
+ const icon = op.action === 'create' ? '✚' : op.action === 'delete' ? '✖' : '✎';
265
+ const iconColor = op.action === 'create' ? colors.green : op.action === 'delete' ? colors.red : colors.yellow;
266
+
267
+ let detail = '';
268
+ if (op.action === 'edit' && fileManager.exists(op.path)) {
269
+ const existing = fileManager.readFile(op.path).content;
270
+ const { added, removed } = getChangeSummary(existing, op.content);
271
+ detail = ` ${colors.dim}(${colors.green}+${added}${colors.dim}/${colors.red}-${removed}${colors.dim})${colors.reset}`;
272
+ } else if (op.action === 'create') {
273
+ const lineCount = op.content.split('\n').length;
274
+ detail = ` ${colors.dim}(${lineCount} lines)${colors.reset}`;
275
+ }
276
+
277
+ lines.push(` ${iconColor}${icon}${colors.reset} ${op.path}${detail}`);
278
+ }
279
+
280
+ lines.push('');
281
+ return lines.join('\n');
282
+ }
283
+
284
+ module.exports = {
285
+ generateDiff,
286
+ prettyDiff,
287
+ showNewFile,
288
+ showDeleteFile,
289
+ showEdit,
290
+ getChangeSummary,
291
+ formatOperation,
292
+ formatOperationsBatch,
293
+ formatCompactSummary,
294
+ colors
295
+ };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * File Manager - Read/write files with backup support
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // File extensions to always skip
9
+ const BINARY_EXTENSIONS = [
10
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
11
+ '.mp3', '.mp4', '.wav', '.avi', '.mov', '.mkv',
12
+ '.zip', '.tar', '.gz', '.rar', '.7z',
13
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
14
+ '.exe', '.dll', '.so', '.dylib',
15
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
16
+ '.lock', '.bin', '.dat'
17
+ ];
18
+
19
+ // Max file size to read (100KB)
20
+ const MAX_FILE_SIZE = 100 * 1024;
21
+
22
+ class FileManager {
23
+ constructor(projectDir) {
24
+ this.projectDir = projectDir;
25
+ this.backupDir = path.join(projectDir, '.banana', 'backups');
26
+ }
27
+
28
+ /**
29
+ * Ensure backup directory exists
30
+ */
31
+ ensureBackupDir() {
32
+ if (!fs.existsSync(this.backupDir)) {
33
+ fs.mkdirSync(this.backupDir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if file should be skipped (binary, too large, etc.)
39
+ */
40
+ shouldSkipFile(filePath) {
41
+ const ext = path.extname(filePath).toLowerCase();
42
+ if (BINARY_EXTENSIONS.includes(ext)) {
43
+ return { skip: true, reason: 'binary file' };
44
+ }
45
+
46
+ try {
47
+ const stats = fs.statSync(filePath);
48
+ if (stats.size > MAX_FILE_SIZE) {
49
+ return { skip: true, reason: `file too large (${Math.round(stats.size / 1024)}KB)` };
50
+ }
51
+ } catch {
52
+ return { skip: true, reason: 'cannot read file stats' };
53
+ }
54
+
55
+ return { skip: false };
56
+ }
57
+
58
+ /**
59
+ * Read a file's contents
60
+ */
61
+ readFile(filePath) {
62
+ const fullPath = path.isAbsolute(filePath)
63
+ ? filePath
64
+ : path.join(this.projectDir, filePath);
65
+
66
+ const skipCheck = this.shouldSkipFile(fullPath);
67
+ if (skipCheck.skip) {
68
+ return { success: false, error: skipCheck.reason };
69
+ }
70
+
71
+ try {
72
+ const content = fs.readFileSync(fullPath, 'utf-8');
73
+ return { success: true, content, path: fullPath };
74
+ } catch (error) {
75
+ return { success: false, error: error.message };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Write content to a file (with backup)
81
+ */
82
+ writeFile(filePath, content) {
83
+ const fullPath = path.isAbsolute(filePath)
84
+ ? filePath
85
+ : path.join(this.projectDir, filePath);
86
+
87
+ try {
88
+ // Create backup if file exists
89
+ if (fs.existsSync(fullPath)) {
90
+ this.ensureBackupDir();
91
+ const timestamp = Date.now();
92
+ const relativePath = path.relative(this.projectDir, fullPath);
93
+ const backupName = `${relativePath.replace(/[/\\]/g, '_')}.${timestamp}.bak`;
94
+ const backupPath = path.join(this.backupDir, backupName);
95
+
96
+ fs.copyFileSync(fullPath, backupPath);
97
+ }
98
+
99
+ // Ensure directory exists
100
+ const dir = path.dirname(fullPath);
101
+ if (!fs.existsSync(dir)) {
102
+ fs.mkdirSync(dir, { recursive: true });
103
+ }
104
+
105
+ // Write the file
106
+ fs.writeFileSync(fullPath, content, 'utf-8');
107
+
108
+ return { success: true, path: fullPath, isNew: !fs.existsSync(fullPath) };
109
+ } catch (error) {
110
+ return { success: false, error: error.message };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Delete a file (with backup)
116
+ */
117
+ deleteFile(filePath) {
118
+ const fullPath = path.isAbsolute(filePath)
119
+ ? filePath
120
+ : path.join(this.projectDir, filePath);
121
+
122
+ try {
123
+ if (!fs.existsSync(fullPath)) {
124
+ return { success: false, error: 'File does not exist' };
125
+ }
126
+
127
+ // Create backup before deleting
128
+ this.ensureBackupDir();
129
+ const timestamp = Date.now();
130
+ const relativePath = path.relative(this.projectDir, fullPath);
131
+ const backupName = `${relativePath.replace(/[/\\]/g, '_')}.${timestamp}.deleted.bak`;
132
+ const backupPath = path.join(this.backupDir, backupName);
133
+
134
+ fs.copyFileSync(fullPath, backupPath);
135
+ fs.unlinkSync(fullPath);
136
+
137
+ return { success: true, path: fullPath };
138
+ } catch (error) {
139
+ return { success: false, error: error.message };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get list of backups
145
+ */
146
+ getBackups() {
147
+ if (!fs.existsSync(this.backupDir)) {
148
+ return [];
149
+ }
150
+
151
+ try {
152
+ const files = fs.readdirSync(this.backupDir);
153
+ return files
154
+ .map(file => {
155
+ const stats = fs.statSync(path.join(this.backupDir, file));
156
+ return {
157
+ name: file,
158
+ path: path.join(this.backupDir, file),
159
+ timestamp: stats.mtime
160
+ };
161
+ })
162
+ .sort((a, b) => b.timestamp - a.timestamp);
163
+ } catch {
164
+ return [];
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Restore most recent backup for a file
170
+ */
171
+ restoreLatest(filePath) {
172
+ const relativePath = path.relative(this.projectDir,
173
+ path.isAbsolute(filePath) ? filePath : path.join(this.projectDir, filePath)
174
+ );
175
+ const searchPrefix = relativePath.replace(/[/\\]/g, '_') + '.';
176
+
177
+ const backups = this.getBackups().filter(b => b.name.startsWith(searchPrefix));
178
+
179
+ if (backups.length === 0) {
180
+ return { success: false, error: 'No backups found for this file' };
181
+ }
182
+
183
+ const latestBackup = backups[0];
184
+
185
+ try {
186
+ const content = fs.readFileSync(latestBackup.path, 'utf-8');
187
+ const targetPath = path.isAbsolute(filePath)
188
+ ? filePath
189
+ : path.join(this.projectDir, filePath);
190
+
191
+ fs.writeFileSync(targetPath, content, 'utf-8');
192
+
193
+ return { success: true, restored: latestBackup.name };
194
+ } catch (error) {
195
+ return { success: false, error: error.message };
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Check if a path exists
201
+ */
202
+ exists(filePath) {
203
+ const fullPath = path.isAbsolute(filePath)
204
+ ? filePath
205
+ : path.join(this.projectDir, filePath);
206
+ return fs.existsSync(fullPath);
207
+ }
208
+
209
+ /**
210
+ * Check if path is a directory
211
+ */
212
+ isDirectory(filePath) {
213
+ const fullPath = path.isAbsolute(filePath)
214
+ ? filePath
215
+ : path.join(this.projectDir, filePath);
216
+ try {
217
+ return fs.statSync(fullPath).isDirectory();
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ }
223
+
224
+ module.exports = FileManager;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Command history manager with readline integration
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ class HistoryManager {
9
+ constructor(bananaDir, maxHistory = 100) {
10
+ this.historyFile = path.join(bananaDir, 'command_history.txt');
11
+ this.maxHistory = maxHistory;
12
+ this.history = [];
13
+ this.historyIndex = -1;
14
+ this.currentInput = '';
15
+ this.load();
16
+ }
17
+
18
+ load() {
19
+ try {
20
+ let filePath = this.historyFile;
21
+ // Migrate from .ripley/ history if .banana/ history doesn't exist
22
+ if (!fs.existsSync(filePath)) {
23
+ const legacyPath = filePath.replace(/\.banana/, '.ripley');
24
+ if (legacyPath !== filePath && fs.existsSync(legacyPath)) {
25
+ filePath = legacyPath;
26
+ }
27
+ }
28
+ if (fs.existsSync(filePath)) {
29
+ const content = fs.readFileSync(filePath, 'utf-8');
30
+ this.history = content.split('\n').filter(line => line.trim());
31
+ // Keep only recent history
32
+ if (this.history.length > this.maxHistory) {
33
+ this.history = this.history.slice(-this.maxHistory);
34
+ }
35
+ // If we read from legacy path, save to new path
36
+ if (filePath !== this.historyFile) {
37
+ this.save();
38
+ }
39
+ }
40
+ } catch {
41
+ this.history = [];
42
+ }
43
+ }
44
+
45
+ save() {
46
+ try {
47
+ const dir = path.dirname(this.historyFile);
48
+ if (!fs.existsSync(dir)) {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ }
51
+ fs.writeFileSync(this.historyFile, this.history.join('\n'));
52
+ } catch {
53
+ // Ignore save errors
54
+ }
55
+ }
56
+
57
+ add(command) {
58
+ if (!command.trim()) return;
59
+
60
+ // Don't add duplicates of the last command
61
+ if (this.history.length > 0 && this.history[this.history.length - 1] === command) {
62
+ return;
63
+ }
64
+
65
+ this.history.push(command);
66
+
67
+ // Trim history if too long
68
+ if (this.history.length > this.maxHistory) {
69
+ this.history = this.history.slice(-this.maxHistory);
70
+ }
71
+
72
+ this.resetIndex();
73
+ this.save();
74
+ }
75
+
76
+ resetIndex() {
77
+ this.historyIndex = this.history.length;
78
+ this.currentInput = '';
79
+ }
80
+
81
+ up(currentLine) {
82
+ if (this.historyIndex === this.history.length) {
83
+ this.currentInput = currentLine;
84
+ }
85
+
86
+ if (this.historyIndex > 0) {
87
+ this.historyIndex--;
88
+ return this.history[this.historyIndex];
89
+ }
90
+
91
+ return this.history[0] || currentLine;
92
+ }
93
+
94
+ down(currentLine) {
95
+ if (this.historyIndex < this.history.length - 1) {
96
+ this.historyIndex++;
97
+ return this.history[this.historyIndex];
98
+ } else if (this.historyIndex === this.history.length - 1) {
99
+ this.historyIndex = this.history.length;
100
+ return this.currentInput;
101
+ }
102
+
103
+ return currentLine;
104
+ }
105
+
106
+ search(prefix) {
107
+ const matches = this.history.filter(cmd =>
108
+ cmd.toLowerCase().startsWith(prefix.toLowerCase())
109
+ );
110
+ return matches;
111
+ }
112
+
113
+ getRecent(count = 10) {
114
+ return this.history.slice(-count);
115
+ }
116
+
117
+ clear() {
118
+ this.history = [];
119
+ this.resetIndex();
120
+ this.save();
121
+ }
122
+ }
123
+
124
+ module.exports = HistoryManager;