@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.
- package/.claude/settings.local.json +8 -0
- package/.mentis/session.json +15 -0
- package/.mentis/sessions/1769189035730.json +23 -0
- package/.mentis/sessions/1769189569160.json +23 -0
- package/.mentis/sessions/1769767538672.json +23 -0
- package/.mentis/sessions/1769767785155.json +23 -0
- package/.mentis/sessions/1769768745802.json +23 -0
- package/.mentis/sessions/1769769600884.json +31 -0
- package/.mentis/sessions/1769770030160.json +31 -0
- package/.mentis/sessions/1769770606004.json +78 -0
- package/.mentis/sessions/1769771084515.json +141 -0
- package/.mentis/sessions/1769881926630.json +57 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -0
- package/dist/debug_google.js +61 -0
- package/dist/debug_lite.js +49 -0
- package/dist/debug_lite_headers.js +57 -0
- package/dist/debug_search.js +16 -0
- package/dist/index.js +10 -0
- package/dist/mcp/JsonRpcClient.js +16 -0
- package/dist/mcp/McpConfig.js +132 -0
- package/dist/mcp/McpManager.js +189 -0
- package/dist/repl/PersistentShell.js +20 -1
- package/dist/repl/ReplManager.js +410 -138
- package/dist/tools/AskQuestionTool.js +172 -0
- package/dist/tools/EditFileTool.js +141 -0
- package/dist/tools/FileTools.js +7 -1
- package/dist/tools/PlanModeTool.js +53 -0
- package/dist/tools/WebSearchTool.js +190 -27
- package/dist/ui/DiffViewer.js +110 -0
- package/dist/ui/InputBox.js +16 -2
- package/dist/ui/MultiFileSelector.js +123 -0
- package/dist/ui/PlanModeUI.js +105 -0
- package/dist/ui/ToolExecutor.js +154 -0
- package/dist/ui/UIManager.js +12 -2
- package/docs/MCP_INTEGRATION.md +290 -0
- package/google_dump.html +18 -0
- package/lite_dump.html +176 -0
- package/lite_headers_dump.html +176 -0
- package/package.json +16 -5
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- package/src/debug_google.ts +30 -0
- package/src/debug_lite.ts +18 -0
- package/src/debug_lite_headers.ts +25 -0
- package/src/debug_search.ts +18 -0
- package/src/index.ts +12 -0
- package/src/mcp/JsonRpcClient.ts +19 -0
- package/src/mcp/McpConfig.ts +153 -0
- package/src/mcp/McpManager.ts +224 -0
- package/src/repl/PersistentShell.ts +24 -1
- package/src/repl/ReplManager.ts +1521 -1204
- package/src/tools/AskQuestionTool.ts +197 -0
- package/src/tools/EditFileTool.ts +172 -0
- package/src/tools/FileTools.ts +3 -0
- package/src/tools/PlanModeTool.ts +50 -0
- package/src/tools/WebSearchTool.ts +235 -63
- package/src/ui/DiffViewer.ts +117 -0
- package/src/ui/InputBox.ts +17 -2
- package/src/ui/MultiFileSelector.ts +135 -0
- package/src/ui/PlanModeUI.ts +121 -0
- package/src/ui/ToolExecutor.ts +182 -0
- package/src/ui/UIManager.ts +15 -2
- package/console.log(tick) +0 -0
|
@@ -1,63 +1,235 @@
|
|
|
1
|
-
import { Tool } from './Tool';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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=([^&]+)&[^"]+">[^<]*<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(/&/g, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>')
|
|
232
|
+
.replace(/"/g, '"')
|
|
233
|
+
.replace(/'/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
|
+
}
|
package/src/ui/InputBox.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|