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