@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.
- 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/ARCHITECTURE.md +267 -0
- package/CONTRIBUTING.md +209 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -0
- package/dist/commands/Command.js +15 -1
- package/dist/commands/CommandManager.js +30 -5
- package/dist/commands/__tests__/CommandManager.test.js +70 -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 +33 -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/skills/Skill.js +17 -2
- package/dist/skills/SkillsManager.js +28 -3
- package/dist/skills/__tests__/SkillsManager.test.js +62 -0
- 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/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- 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 +34 -2
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- package/src/commands/Command.ts +64 -13
- package/src/commands/CommandManager.ts +26 -5
- package/src/commands/__tests__/CommandManager.test.ts +83 -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 +45 -1
- 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/skills/Skill.ts +91 -11
- package/src/skills/SkillsManager.ts +25 -3
- package/src/skills/__tests__/SkillsManager.test.ts +73 -0
- 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/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
- 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
|
|
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
|
-
|
|
26
|
-
|
|
34
|
+
// 1. Try API Key (Most Reliable)
|
|
35
|
+
if (process.env.TAVILY_API_KEY) {
|
|
27
36
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 (
|
|
41
|
-
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Ignore npm error
|
|
42
54
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 `
|
|
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=([^&]+)&[^"]+">[^<]*<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(/&/g, '&')
|
|
217
|
+
.replace(/</g, '<')
|
|
218
|
+
.replace(/>/g, '>')
|
|
219
|
+
.replace(/"/g, '"')
|
|
220
|
+
.replace(/'/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;
|
package/dist/ui/InputBox.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 = [];
|