@indiccoder/mentis-cli 1.1.4 → 1.1.5

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 (64) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.mentis/session.json +15 -0
  3. package/.mentis/sessions/1769189035730.json +23 -0
  4. package/.mentis/sessions/1769189569160.json +23 -0
  5. package/.mentis/sessions/1769767538672.json +23 -0
  6. package/.mentis/sessions/1769767785155.json +23 -0
  7. package/.mentis/sessions/1769768745802.json +23 -0
  8. package/.mentis/sessions/1769769600884.json +31 -0
  9. package/.mentis/sessions/1769770030160.json +31 -0
  10. package/.mentis/sessions/1769770606004.json +78 -0
  11. package/.mentis/sessions/1769771084515.json +141 -0
  12. package/.mentis/sessions/1769881926630.json +57 -0
  13. package/README.md +17 -0
  14. package/dist/checkpoint/CheckpointManager.js +92 -0
  15. package/dist/debug_google.js +61 -0
  16. package/dist/debug_lite.js +49 -0
  17. package/dist/debug_lite_headers.js +57 -0
  18. package/dist/debug_search.js +16 -0
  19. package/dist/index.js +10 -0
  20. package/dist/mcp/JsonRpcClient.js +16 -0
  21. package/dist/mcp/McpConfig.js +132 -0
  22. package/dist/mcp/McpManager.js +189 -0
  23. package/dist/repl/PersistentShell.js +20 -1
  24. package/dist/repl/ReplManager.js +410 -138
  25. package/dist/tools/AskQuestionTool.js +172 -0
  26. package/dist/tools/EditFileTool.js +141 -0
  27. package/dist/tools/FileTools.js +7 -1
  28. package/dist/tools/PlanModeTool.js +53 -0
  29. package/dist/tools/WebSearchTool.js +190 -27
  30. package/dist/ui/DiffViewer.js +110 -0
  31. package/dist/ui/InputBox.js +16 -2
  32. package/dist/ui/MultiFileSelector.js +123 -0
  33. package/dist/ui/PlanModeUI.js +105 -0
  34. package/dist/ui/ToolExecutor.js +154 -0
  35. package/dist/ui/UIManager.js +12 -2
  36. package/docs/MCP_INTEGRATION.md +290 -0
  37. package/google_dump.html +18 -0
  38. package/lite_dump.html +176 -0
  39. package/lite_headers_dump.html +176 -0
  40. package/package.json +16 -5
  41. package/scripts/test_exa_mcp.ts +90 -0
  42. package/src/checkpoint/CheckpointManager.ts +102 -0
  43. package/src/debug_google.ts +30 -0
  44. package/src/debug_lite.ts +18 -0
  45. package/src/debug_lite_headers.ts +25 -0
  46. package/src/debug_search.ts +18 -0
  47. package/src/index.ts +12 -0
  48. package/src/mcp/JsonRpcClient.ts +19 -0
  49. package/src/mcp/McpConfig.ts +153 -0
  50. package/src/mcp/McpManager.ts +224 -0
  51. package/src/repl/PersistentShell.ts +24 -1
  52. package/src/repl/ReplManager.ts +1521 -1204
  53. package/src/tools/AskQuestionTool.ts +197 -0
  54. package/src/tools/EditFileTool.ts +172 -0
  55. package/src/tools/FileTools.ts +3 -0
  56. package/src/tools/PlanModeTool.ts +50 -0
  57. package/src/tools/WebSearchTool.ts +235 -63
  58. package/src/ui/DiffViewer.ts +117 -0
  59. package/src/ui/InputBox.ts +17 -2
  60. package/src/ui/MultiFileSelector.ts +135 -0
  61. package/src/ui/PlanModeUI.ts +121 -0
  62. package/src/ui/ToolExecutor.ts +182 -0
  63. package/src/ui/UIManager.ts +15 -2
  64. package/console.log(tick) +0 -0
@@ -1,63 +1,235 @@
1
- import { Tool } from './Tool';
2
- import { search } from 'duck-duck-scrape';
3
- import chalk from 'chalk';
4
-
5
- export class WebSearchTool implements Tool {
6
- name = 'search_web';
7
- description = 'Search the internet for documentation, libraries, or solutions to errors. Returns snippets of top results.';
8
- parameters = {
9
- type: 'object',
10
- properties: {
11
- query: {
12
- type: 'string',
13
- description: 'The search query.'
14
- }
15
- },
16
- required: ['query']
17
- };
18
-
19
- async execute(args: { query: string }): Promise<string> {
20
- try {
21
- // Priority 1: Google Search
22
- try {
23
- // Dynamic import to avoid build issues if types are missing
24
- const { search: googleSearch } = require('google-sr');
25
- console.log(chalk.dim(` Searching Google for: "${args.query}"...`));
26
-
27
- const googleResults: any[] = await googleSearch({
28
- query: args.query,
29
- limit: 5,
30
- });
31
-
32
- if (googleResults && googleResults.length > 0) {
33
- const formatted = googleResults.map(r =>
34
- `[${r.title}](${r.link})\n${r.description || 'No description.'}`
35
- ).join('\n\n');
36
- return `Top Google Results:\n\n${formatted}`;
37
- }
38
- } catch (googleError: any) {
39
- console.log(chalk.dim(` Google search failed (${googleError.message}), failing over to DuckDuckGo...`));
40
- }
41
-
42
- // Priority 2: DuckDuckGo Fallback
43
- console.log(chalk.dim(` Searching DuckDuckGo for: "${args.query}"...`));
44
- const ddgResults = await search(args.query, {
45
- safeSearch: 0
46
- });
47
-
48
- if (!ddgResults.results || ddgResults.results.length === 0) {
49
- return 'No results found.';
50
- }
51
-
52
- // Return top 5 results
53
- const topResults = ddgResults.results.slice(0, 5).map(r =>
54
- `[${r.title}](${r.url})\n${r.description || 'No description found.'}`
55
- ).join('\n\n');
56
-
57
- return `Top Search Results (via DDG):\n\n${topResults}`;
58
-
59
- } catch (error: any) {
60
- return `Error searching web: ${error.message}`;
61
- }
62
- }
63
- }
1
+ import { Tool } from './Tool';
2
+ import chalk from 'chalk';
3
+ import { exec } from 'child_process';
4
+ import os from 'os';
5
+
6
+ export class WebSearchTool implements Tool {
7
+ name = 'search_web';
8
+ description = 'Search internet for documentation, libraries, or solutions to errors. Returns snippets of top results.';
9
+ parameters = {
10
+ type: 'object',
11
+ properties: {
12
+ query: {
13
+ type: 'string',
14
+ description: 'The search query.'
15
+ }
16
+ },
17
+ required: ['query']
18
+ };
19
+
20
+ /**
21
+ * Execute search using a hybrid strategy:
22
+ * 1. Tavily API (if configured) - Most Reliable
23
+ * 2. NPM/Expo Registry (if applicable) - Bypasses search engines
24
+ * 3. Google Curl - Mimics browser request
25
+ * 4. DuckDuckGo Lite - Low bandwidth fallback
26
+ * 5. DuckDuckScrape Library - Last resort
27
+ */
28
+ async execute(args: { query: string }): Promise<string> {
29
+ // 1. Try API Key (Most Reliable)
30
+ if (process.env.TAVILY_API_KEY) {
31
+ try {
32
+ return await this.searchTavily(args.query, process.env.TAVILY_API_KEY);
33
+ } catch (e) {
34
+ console.error(chalk.red('Tavily search failed, falling back to scraper.'));
35
+ }
36
+ }
37
+
38
+ // 2. Specific Fallback: NPM/Expo Queries (Bypass Search Engines)
39
+ // If user asks for versions, use npm directly
40
+ if (args.query.toLowerCase().includes('expo') || args.query.toLowerCase().includes('react native')) {
41
+ try {
42
+ const npmInfo = await this.checkNpmVersion('expo');
43
+ if (npmInfo) {
44
+ return `NPM Registry Info:\n${npmInfo}\n\n(Web search was blocked, but I checked NPM directly.)`;
45
+ }
46
+ } catch (e) {
47
+ // Ignore npm error
48
+ }
49
+ }
50
+
51
+ // Rate limit protection
52
+ await new Promise(resolve => setTimeout(resolve, 2000));
53
+
54
+ try {
55
+ // Strategy 1: Google Curl (Specific Browser Header)
56
+ try {
57
+ const googleResults = await this.searchGoogleCurl(args.query);
58
+ if (googleResults.length > 0) return this.formatResults(googleResults, 'Google');
59
+ } catch (e) {
60
+ // Ignore
61
+ }
62
+
63
+ // Strategy 2: DDG Lite
64
+ try {
65
+ const liteResults = await this.searchDuckDuckGoLite(args.query);
66
+ if (liteResults.length > 0) return this.formatResults(liteResults, 'DDG Lite');
67
+ } catch (e) {
68
+ // Ignore
69
+ }
70
+
71
+ // Strategy 3: Library Fallback
72
+ console.log(chalk.dim(` Direct scraping failed, falling back to library...`));
73
+ const { search } = require('duck-duck-scrape');
74
+ const ddgResults = await search(args.query, { safeSearch: 0 });
75
+
76
+ if (!ddgResults.results?.length) throw new Error('No results from library');
77
+
78
+ return `Top Search Results (Library):\n\n` +
79
+ ddgResults.results.slice(0, 5).map((r: any) =>
80
+ `[${r.title}](${r.url})\n${r.description || 'No description found.'}`
81
+ ).join('\n\n');
82
+
83
+ } catch (error: any) {
84
+ return `Web Search Failed (CAPTCHA Blocked).
85
+
86
+ Search engines are blocking automated requests from your network.
87
+
88
+ To enable web search, get a free Tavily API key:
89
+ 1. Go to https://tavily.com
90
+ 2. Sign up for free
91
+ 3. Add to your .env: TAVILY_API_KEY=your_key_here
92
+
93
+ Alternatively, use Exa via MCP:
94
+ 1. Get key at https://exa.ai
95
+ 2. Add EXA_API_KEY to .env
96
+ 3. Run: /mcp connect "Exa Search"`;
97
+ }
98
+ }
99
+
100
+ private formatResults(results: any[], source: string): string {
101
+ const formatted = results.slice(0, 5).map(r =>
102
+ `[${r.title}](${r.url})\n${r.snippet || 'No description.'}`
103
+ ).join('\n\n');
104
+ return `Top Search Results (${source}):\n\n${formatted}`;
105
+ }
106
+
107
+ private async searchTavily(query: string, apiKey: string): Promise<string> {
108
+ console.log(chalk.dim(` Searching Tavily for: "${query}"...`));
109
+ try {
110
+ const response = await fetch("https://api.tavily.com/search", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ api_key: apiKey, query, search_depth: "basic", include_answer: true })
114
+ });
115
+ const data = await response.json() as any;
116
+ const results = data.results.map((r: any) => `[${r.title}](${r.url})\n${r.content}`).join('\n\n');
117
+ return `Tavily Results:\n\n${results}`;
118
+ } catch (e: any) {
119
+ throw new Error(`Tavily Error: ${e.message}`);
120
+ }
121
+ }
122
+
123
+ private async checkNpmVersion(pkg: string): Promise<string> {
124
+ return new Promise((resolve) => {
125
+ exec(`npm view ${pkg} name version dist-tags --json`, { maxBuffer: 1024 * 1024 }, (error, stdout) => {
126
+ if (error || !stdout) resolve('');
127
+ try {
128
+ const info = JSON.parse(stdout);
129
+ resolve(`Package: ${info.name}\nLatest Version: ${info.version}\nTags: ${JSON.stringify(info['dist-tags'])}`);
130
+ } catch { resolve('') }
131
+ });
132
+ });
133
+ }
134
+
135
+ private async searchGoogleCurl(query: string): Promise<Array<{ title: string, url: string, snippet: string }>> {
136
+ console.log(chalk.dim(` Searching Google (Curl) for: "${query}"...`));
137
+ const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&hl=en`;
138
+ const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
139
+
140
+ // Use curl.exe explicitly on Windows
141
+ const curlCmd = os.platform() === 'win32' ? 'curl.exe' : 'curl';
142
+
143
+ return new Promise((resolve, reject) => {
144
+ exec(`${curlCmd} -s -L -A "${userAgent}" "${url}"`, { maxBuffer: 1024 * 1024 * 2 }, (error, stdout) => {
145
+ if (error) {
146
+ reject(error);
147
+ return;
148
+ }
149
+
150
+ const html = stdout;
151
+ const results: Array<{ title: string, url: string, snippet: string }> = [];
152
+
153
+ // Matches standard google result anchors: <a href="/url?q=..." ...><h3 ...>Title</h3>...
154
+ const linkRegex = /<a href="\/url\?q=([^&]+)&amp;[^"]+">[^<]*<h3[^>]*><div[^>]*>([^<]+)<\/div><\/h3>/g;
155
+
156
+ let match;
157
+ while ((match = linkRegex.exec(html)) !== null) {
158
+ results.push({
159
+ url: decodeURIComponent(match[1]),
160
+ title: this.decodeHtml(match[2]),
161
+ snippet: ''
162
+ });
163
+ }
164
+
165
+ resolve(results);
166
+ });
167
+ });
168
+ }
169
+
170
+ private async curl(url: string): Promise<string> {
171
+ const curl = os.platform() === 'win32' ? 'curl.exe' : 'curl';
172
+ const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
173
+
174
+ return new Promise<string>((resolve, reject) => {
175
+ exec(`${curl} -s -L -A "${userAgent}" "${url}"`, { maxBuffer: 1024 * 1024 * 2 }, (error, stdout, stderr) => {
176
+ if (error) {
177
+ reject(error);
178
+ return;
179
+ }
180
+ resolve(stdout);
181
+ });
182
+ });
183
+ }
184
+
185
+ private async searchDuckDuckGoHtml(query: string): Promise<Array<{ title: string, url: string, snippet: string }>> {
186
+ console.log(chalk.dim(` Searching DuckDuckGo (HTML) for: "${query}"...`));
187
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
188
+ const html = await this.curl(url);
189
+
190
+ const results: Array<{ title: string, url: string, snippet: string }> = [];
191
+ const chunks = html.split('class="result__body"');
192
+
193
+ for (let i = 1; i < chunks.length; i++) {
194
+ const chunk = chunks[i];
195
+ const titleMatch = chunk.match(/class="result__a" href="([^"]+)">(.*?)<\/a>/);
196
+ const snippetMatch = chunk.match(/class="result__snippet"[^>]*>(.*?)<\/a>/);
197
+
198
+ if (titleMatch) {
199
+ results.push({
200
+ url: titleMatch[1],
201
+ title: this.decodeHtml(titleMatch[2].replace(/<[^>]+>/g, '').trim()),
202
+ snippet: snippetMatch ? this.decodeHtml(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''
203
+ });
204
+ }
205
+ }
206
+ return results;
207
+ }
208
+
209
+ private async searchDuckDuckGoLite(query: string): Promise<Array<{ title: string, url: string, snippet: string }>> {
210
+ console.log(chalk.dim(` Searching DuckDuckGo (Lite) for: "${query}"...`));
211
+ const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
212
+ const html = await this.curl(url);
213
+
214
+ const results: Array<{ title: string, url: string, snippet: string }> = [];
215
+ const regex = /<a[^>]+class="result-link"[^>]+href="(.*?)"[^>]*>(.*?)<\/a>/g;
216
+ let match;
217
+ while ((match = regex.exec(html)) !== null) {
218
+ results.push({
219
+ url: match[1],
220
+ title: this.decodeHtml(match[2].replace(/<[^>]+>/g, '').trim()),
221
+ snippet: 'Click to view.'
222
+ });
223
+ }
224
+ return results;
225
+ }
226
+
227
+ private decodeHtml(str: string): string {
228
+ return str
229
+ .replace(/&amp;/g, '&')
230
+ .replace(/&lt;/g, '<')
231
+ .replace(/&gt;/g, '>')
232
+ .replace(/&quot;/g, '"')
233
+ .replace(/&#39;/g, "'");
234
+ }
235
+ }
@@ -0,0 +1,117 @@
1
+ import chalk from 'chalk';
2
+ import { diffLines } from 'diff';
3
+
4
+ /**
5
+ * Visual diff viewer component
6
+ * Shows file changes with color coding like git diff
7
+ */
8
+ export class DiffViewer {
9
+ /**
10
+ * Display a unified diff between old and new content
11
+ */
12
+ static showDiff(filePath: string, oldContent: string, newContent: string, contextLines: number = 3): void {
13
+ const diff = diffLines(oldContent, newContent);
14
+
15
+ console.log('');
16
+ console.log(chalk.gray('─'.repeat(60)));
17
+ console.log(chalk.cyan(`📝 Diff: ${filePath}`));
18
+ console.log(chalk.gray('─'.repeat(60)));
19
+ console.log('');
20
+
21
+ let unchangedCount = 0;
22
+ const unchangedBuffer: string[] = [];
23
+
24
+ const flushUnchanged = () => {
25
+ if (unchangedBuffer.length > 0) {
26
+ // Show context lines
27
+ const contextStart = Math.max(0, unchangedBuffer.length - contextLines);
28
+ for (let i = contextStart; i < unchangedBuffer.length; i++) {
29
+ console.log(chalk.dim(' ' + unchangedBuffer[i].replace(/\n/g, '')));
30
+ }
31
+ unchangedBuffer.length = 0;
32
+ unchangedCount = 0;
33
+ }
34
+ };
35
+
36
+ let hasChanges = false;
37
+ let additions = 0;
38
+ let deletions = 0;
39
+
40
+ for (const part of diff) {
41
+ const lines = part.value.split('\n');
42
+ // Remove empty last line if exists
43
+ if (lines[lines.length - 1] === '') {
44
+ lines.pop();
45
+ }
46
+
47
+ for (const line of lines) {
48
+ if (part.added) {
49
+ flushUnchanged();
50
+ console.log(chalk.green('+ ' + line));
51
+ additions++;
52
+ hasChanges = true;
53
+ } else if (part.removed) {
54
+ flushUnchanged();
55
+ console.log(chalk.red('- ' + line));
56
+ deletions++;
57
+ hasChanges = true;
58
+ } else {
59
+ unchangedBuffer.push(line);
60
+ unchangedCount++;
61
+ }
62
+ }
63
+ }
64
+
65
+ flushUnchanged();
66
+
67
+ console.log('');
68
+ console.log(chalk.gray('─'.repeat(60)));
69
+
70
+ if (hasChanges) {
71
+ console.log(chalk.green(`+ ${additions} additions`) + chalk.dim(' | ') + chalk.red(`- ${deletions} deletions`));
72
+ } else {
73
+ console.log(chalk.dim('No changes'));
74
+ }
75
+
76
+ console.log(chalk.gray('─'.repeat(60)));
77
+ console.log('');
78
+ }
79
+
80
+ /**
81
+ * Display a simple edit preview (for EditFileTool)
82
+ */
83
+ static showEditPreview(filePath: string, oldString: string, newString: string, lineNumber: number): void {
84
+ console.log('');
85
+ console.log(chalk.gray('─'.repeat(60)));
86
+ console.log(chalk.cyan(`📝 Edit Preview: ${filePath}`));
87
+ console.log(chalk.gray('─'.repeat(60)));
88
+ console.log(chalk.dim(`Line ${lineNumber}:`));
89
+ console.log('');
90
+
91
+ const oldLines = oldString.split('\n');
92
+ const newLines = newString.split('\n');
93
+
94
+ // Show removed lines in red
95
+ for (const line of oldLines) {
96
+ console.log(chalk.red('- ' + line));
97
+ }
98
+
99
+ // Show added lines in green
100
+ for (const line of newLines) {
101
+ console.log(chalk.green('+ ' + line));
102
+ }
103
+
104
+ console.log('');
105
+ console.log(chalk.gray('─'.repeat(60)));
106
+ console.log('');
107
+ }
108
+
109
+ /**
110
+ * Display approval prompt
111
+ */
112
+ static showApprovalPrompt(filePath: string, operation: 'write' | 'edit'): void {
113
+ const icon = operation === 'write' ? '📄' : '✏️';
114
+ console.log(chalk.yellow(`${icon} Approve ${operation} to ${filePath}?`));
115
+ console.log(chalk.dim(' [y] Yes [n] No [e] Edit'));
116
+ }
117
+ }
@@ -62,19 +62,34 @@ export class InputBox {
62
62
 
63
63
  rl.prompt();
64
64
 
65
+ const cleanup = () => {
66
+ rl.close();
67
+ rl.removeAllListeners();
68
+ // Explicitly ensure raw mode is off to prevent terminal freeze
69
+ if (process.stdin.isTTY) {
70
+ process.stdin.setRawMode(false);
71
+ }
72
+ };
73
+
65
74
  rl.on('line', (line) => {
66
75
  // Display bottom horizontal line after input
67
76
  console.log(this.createLine());
68
- rl.close();
77
+ cleanup();
69
78
  resolve(line);
70
79
  });
71
80
 
72
81
  // Handle Ctrl+C
73
82
  rl.on('SIGINT', () => {
74
83
  console.log(this.createLine());
75
- rl.close();
84
+ cleanup();
76
85
  resolve('/exit');
77
86
  });
87
+
88
+ // Handle stream errors or unexpected close
89
+ rl.on('close', () => {
90
+ // Should already be cleaned up, but safe to ensure
91
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
92
+ });
78
93
  });
79
94
  }
80
95
 
@@ -0,0 +1,135 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { statSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ export interface FileSelection {
7
+ path: string;
8
+ selected: boolean;
9
+ size?: string;
10
+ type?: string;
11
+ }
12
+
13
+ /**
14
+ * Multi-file selector for read approval
15
+ * Shows interactive checklist when AI wants to read multiple files
16
+ */
17
+ export class MultiFileSelector {
18
+ /**
19
+ * Show file selection UI for read operations
20
+ * Returns the list of approved files
21
+ */
22
+ static async selectFiles(filePaths: string[], message: string = 'Select files to read:'): Promise<string[]> {
23
+ if (filePaths.length === 0) {
24
+ return [];
25
+ }
26
+
27
+ if (filePaths.length === 1) {
28
+ // Single file - just show what's being read
29
+ console.log(chalk.dim(`📖 Reading: ${filePaths[0]}`));
30
+ return filePaths;
31
+ }
32
+
33
+ // Build file choices with metadata
34
+ const choices = filePaths.map(path => {
35
+ let metadata = '';
36
+ try {
37
+ const stats = statSync(path);
38
+ const size = this.formatFileSize(stats.size);
39
+ metadata = chalk.dim(` (${size})`);
40
+ } catch {
41
+ // File might not exist or be inaccessible
42
+ }
43
+
44
+ return {
45
+ name: path + metadata,
46
+ value: path,
47
+ checked: true, // Default to checked
48
+ short: path
49
+ };
50
+ });
51
+
52
+ console.log('');
53
+ console.log(chalk.cyan(`📖 AI wants to read ${filePaths.length} files:`));
54
+ console.log('');
55
+
56
+ const { selectedFiles } = await inquirer.prompt([
57
+ {
58
+ type: 'checkbox',
59
+ name: 'selectedFiles',
60
+ message: message,
61
+ choices: choices,
62
+ pageSize: 15,
63
+ validate: (answer: string[]) => {
64
+ if (answer.length === 0) {
65
+ return 'You must select at least one file, or press Ctrl+C to cancel.';
66
+ }
67
+ return true;
68
+ }
69
+ }
70
+ ]);
71
+
72
+ // Show what was selected
73
+ if (selectedFiles.length < filePaths.length) {
74
+ console.log(chalk.dim(` Reading ${selectedFiles.length} of ${filePaths.length} files`));
75
+ }
76
+
77
+ return selectedFiles;
78
+ }
79
+
80
+ /**
81
+ * Show a simple confirmation for single file reads (optional)
82
+ */
83
+ static async confirmRead(filePath: string, preview?: string): Promise<boolean> {
84
+ let message = chalk.cyan(`📖 Read file: ${filePath}?`);
85
+
86
+ if (preview) {
87
+ const lines = preview.split('\n');
88
+ const previewLines = lines.slice(0, 5);
89
+ console.log('');
90
+ console.log(chalk.gray('─'.repeat(60)));
91
+ console.log(message);
92
+ console.log(chalk.gray('─'.repeat(60)));
93
+ console.log(chalk.dim('Preview:'));
94
+ for (const line of previewLines) {
95
+ console.log(chalk.dim(' ' + line));
96
+ }
97
+ if (lines.length > 5) {
98
+ console.log(chalk.dim(' ...'));
99
+ }
100
+ console.log(chalk.gray('─'.repeat(60)));
101
+ } else {
102
+ console.log(message);
103
+ }
104
+
105
+ const { confirmed } = await inquirer.prompt([
106
+ {
107
+ type: 'confirm',
108
+ name: 'confirmed',
109
+ message: 'Continue?',
110
+ default: true
111
+ }
112
+ ]);
113
+
114
+ return confirmed;
115
+ }
116
+
117
+ /**
118
+ * Format file size in human-readable format
119
+ */
120
+ private static formatFileSize(bytes: number): string {
121
+ if (bytes === 0) return '0 B';
122
+ const k = 1024;
123
+ const sizes = ['B', 'KB', 'MB', 'GB'];
124
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
125
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
126
+ }
127
+
128
+ /**
129
+ * Show progress when reading multiple files
130
+ */
131
+ static showReadProgress(current: number, total: number, filePath: string): void {
132
+ const progress = chalk.dim(`[${current}/${total}]`);
133
+ console.log(chalk.dim(` ${progress} Reading: ${filePath}`));
134
+ }
135
+ }