minovative-mind-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +418 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.js +30 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +18 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/agent-tools.d.ts +36 -0
- package/dist/services/agent-tools.js +764 -0
- package/dist/services/agent.d.ts +21 -0
- package/dist/services/agent.js +648 -0
- package/dist/services/ai.d.ts +60 -0
- package/dist/services/ai.js +331 -0
- package/dist/services/auth.d.ts +3 -0
- package/dist/services/auth.js +183 -0
- package/dist/services/changeLogger.d.ts +23 -0
- package/dist/services/changeLogger.js +57 -0
- package/dist/services/contextAgent.d.ts +20 -0
- package/dist/services/contextAgent.js +440 -0
- package/dist/services/proxyClient.d.ts +21 -0
- package/dist/services/proxyClient.js +119 -0
- package/dist/services/verificationService.d.ts +10 -0
- package/dist/services/verificationService.js +148 -0
- package/dist/utils/atomicWrite.d.ts +6 -0
- package/dist/utils/atomicWrite.js +29 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/contextPrompts.d.ts +3 -0
- package/dist/utils/contextPrompts.js +34 -0
- package/dist/utils/dependencyTracer.d.ts +48 -0
- package/dist/utils/dependencyTracer.js +647 -0
- package/dist/utils/excludedExtensions.d.ts +8 -0
- package/dist/utils/excludedExtensions.js +125 -0
- package/dist/utils/fuzzyMatch.d.ts +21 -0
- package/dist/utils/fuzzyMatch.js +121 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +17 -0
- package/dist/utils/pathSecurity.d.ts +10 -0
- package/dist/utils/pathSecurity.js +26 -0
- package/dist/utils/symbolExtractor.d.ts +6 -0
- package/dist/utils/symbolExtractor.js +249 -0
- package/dist/utils/syntaxValidator.d.ts +5 -0
- package/dist/utils/syntaxValidator.js +81 -0
- package/dist/utils/systemPrompts.d.ts +5 -0
- package/dist/utils/systemPrompts.js +119 -0
- package/oclif.manifest.json +69 -0
- package/package.json +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
export declare class AsyncInputHandler {
|
|
3
|
+
private queue;
|
|
4
|
+
private isPrompting;
|
|
5
|
+
private spinner;
|
|
6
|
+
private originalRawMode;
|
|
7
|
+
private stopped;
|
|
8
|
+
private ac;
|
|
9
|
+
setAbortController(ac: AbortController): void;
|
|
10
|
+
isCurrentlyPrompting(): boolean;
|
|
11
|
+
waitForPrompt(): Promise<void>;
|
|
12
|
+
private onData;
|
|
13
|
+
start(spinner?: ReturnType<typeof p.spinner>): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
getAndClear(): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Starts the interactive agent chat loop.
|
|
19
|
+
* Runs until the user types "exit", "quit", or presses Ctrl+C.
|
|
20
|
+
*/
|
|
21
|
+
export declare function startAgentLoop(workspaceRoot: string, version: string): Promise<void>;
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { exec } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export class AsyncInputHandler {
|
|
9
|
+
queue = [];
|
|
10
|
+
isPrompting = false;
|
|
11
|
+
spinner = null;
|
|
12
|
+
originalRawMode = false;
|
|
13
|
+
stopped = true;
|
|
14
|
+
ac = null;
|
|
15
|
+
setAbortController(ac) {
|
|
16
|
+
this.ac = ac;
|
|
17
|
+
}
|
|
18
|
+
isCurrentlyPrompting() {
|
|
19
|
+
return this.isPrompting;
|
|
20
|
+
}
|
|
21
|
+
async waitForPrompt() {
|
|
22
|
+
while (this.isPrompting) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
onData = async (chunk) => {
|
|
27
|
+
try {
|
|
28
|
+
if (this.isPrompting)
|
|
29
|
+
return;
|
|
30
|
+
const char = chunk.toString();
|
|
31
|
+
if (char === '\u0003') {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (char.charCodeAt(0) < 32 || char === '\u007f')
|
|
35
|
+
return;
|
|
36
|
+
this.isPrompting = true;
|
|
37
|
+
process.stdin.removeListener('data', this.onData);
|
|
38
|
+
if (process.stdin.isTTY) {
|
|
39
|
+
process.stdin.setRawMode(false);
|
|
40
|
+
}
|
|
41
|
+
if (this.spinner) {
|
|
42
|
+
this.spinner.stop('Paused to receive input');
|
|
43
|
+
}
|
|
44
|
+
const userInput = await p.text({
|
|
45
|
+
message: 'Add chained message:',
|
|
46
|
+
placeholder: '(Leave blank and press Enter to cancel)',
|
|
47
|
+
initialValue: char,
|
|
48
|
+
});
|
|
49
|
+
if (!p.isCancel(userInput) && userInput.trim()) {
|
|
50
|
+
const text = userInput.trim();
|
|
51
|
+
if (text.toLowerCase() === 'stop' || text.toLowerCase() === 'stop!') {
|
|
52
|
+
if (this.ac) {
|
|
53
|
+
this.ac.abort();
|
|
54
|
+
p.log.warn(pc.yellow(`Generation aborted by user.`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.queue.push(text);
|
|
59
|
+
p.log.info(pc.cyan(`📥 Queued message: "${text}"`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (this.spinner) {
|
|
63
|
+
this.spinner.start('Resuming execution...');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// Ignore
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (!this.stopped) {
|
|
71
|
+
if (process.stdin.isTTY) {
|
|
72
|
+
process.stdin.setRawMode(true);
|
|
73
|
+
}
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (!this.stopped) {
|
|
76
|
+
process.stdin.on('data', this.onData);
|
|
77
|
+
}
|
|
78
|
+
this.isPrompting = false;
|
|
79
|
+
}, 50);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.isPrompting = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
start(spinner) {
|
|
87
|
+
this.stopped = false;
|
|
88
|
+
if (spinner) {
|
|
89
|
+
this.spinner = spinner;
|
|
90
|
+
}
|
|
91
|
+
if (process.stdin.isTTY) {
|
|
92
|
+
this.originalRawMode = process.stdin.isRaw;
|
|
93
|
+
process.stdin.setRawMode(true);
|
|
94
|
+
}
|
|
95
|
+
process.stdin.resume();
|
|
96
|
+
process.stdin.on('data', this.onData);
|
|
97
|
+
}
|
|
98
|
+
stop() {
|
|
99
|
+
this.stopped = true;
|
|
100
|
+
process.stdin.removeListener('data', this.onData);
|
|
101
|
+
if (process.stdin.isTTY) {
|
|
102
|
+
process.stdin.setRawMode(this.originalRawMode);
|
|
103
|
+
}
|
|
104
|
+
this.spinner = null;
|
|
105
|
+
}
|
|
106
|
+
getAndClear() {
|
|
107
|
+
if (this.queue.length === 0)
|
|
108
|
+
return '';
|
|
109
|
+
const messages = this.queue.join('\n');
|
|
110
|
+
this.queue = [];
|
|
111
|
+
return messages;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
import { consumeSkipOnce, executeTool, getApprovalMode, setApprovalMode } from './agent-tools.js';
|
|
115
|
+
import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession } from './ai.js';
|
|
116
|
+
import { changeLogger } from './changeLogger.js';
|
|
117
|
+
import { gatherContext, routeIntent } from './contextAgent.js';
|
|
118
|
+
import { verifyChangedFiles } from './verificationService.js';
|
|
119
|
+
import { buildContextInjection } from '../utils/contextPrompts.js';
|
|
120
|
+
import { debugLog, toggleDebugMode } from '../utils/logger.js';
|
|
121
|
+
import { marked } from 'marked';
|
|
122
|
+
import { markedTerminal } from 'marked-terminal';
|
|
123
|
+
marked.use(markedTerminal());
|
|
124
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
125
|
+
const TOOL_ICONS = {
|
|
126
|
+
read_file: '📖',
|
|
127
|
+
write_file: '✏️',
|
|
128
|
+
modify_file: '🔧',
|
|
129
|
+
list_directory: '📂',
|
|
130
|
+
run_command: '⚡',
|
|
131
|
+
grep_search: '🔍',
|
|
132
|
+
delete_file: '🗑️',
|
|
133
|
+
rename_file: '🚚',
|
|
134
|
+
find_dependencies: '🔗',
|
|
135
|
+
};
|
|
136
|
+
const TOOL_LABELS = {
|
|
137
|
+
read_file: 'Reading file',
|
|
138
|
+
write_file: 'Writing file',
|
|
139
|
+
modify_file: 'Modifying file',
|
|
140
|
+
list_directory: 'Listing directory',
|
|
141
|
+
run_command: 'Running command',
|
|
142
|
+
grep_search: 'Searching code',
|
|
143
|
+
delete_file: 'Deleting file',
|
|
144
|
+
rename_file: 'Moving file',
|
|
145
|
+
find_dependencies: 'Tracing dependencies',
|
|
146
|
+
};
|
|
147
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
148
|
+
function formatToolCall(name, args) {
|
|
149
|
+
const icon = TOOL_ICONS[name] ?? '🔧';
|
|
150
|
+
const label = TOOL_LABELS[name] ?? name;
|
|
151
|
+
switch (name) {
|
|
152
|
+
case 'read_file':
|
|
153
|
+
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
|
|
154
|
+
case 'write_file':
|
|
155
|
+
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
|
|
156
|
+
case 'modify_file':
|
|
157
|
+
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
|
|
158
|
+
case 'list_directory':
|
|
159
|
+
return `${icon} ${label}: ${pc.cyan(String(args.dirPath ?? '.'))}`;
|
|
160
|
+
case 'run_command':
|
|
161
|
+
return `${icon} ${label}: ${pc.yellow(String(args.command))}`;
|
|
162
|
+
case 'grep_search':
|
|
163
|
+
return `${icon} ${label}: ${pc.magenta(String(args.pattern))}`;
|
|
164
|
+
case 'delete_file':
|
|
165
|
+
return `${icon} ${label}: ${pc.red(String(args.filePath))}`;
|
|
166
|
+
case 'rename_file':
|
|
167
|
+
return `${icon} ${label}: ${pc.cyan(String(args.sourcePath))} -> ${pc.cyan(String(args.targetPath))}`;
|
|
168
|
+
case 'find_dependencies':
|
|
169
|
+
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}${args.direction ? ` (${args.direction})` : ''}`;
|
|
170
|
+
default:
|
|
171
|
+
return `${icon} ${label}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Prompts the user to approve a shell command, respecting the current
|
|
176
|
+
* approval mode (ask / skip-once / skip-all).
|
|
177
|
+
*
|
|
178
|
+
* Returns `true` if the command should run, `false` to deny.
|
|
179
|
+
*/
|
|
180
|
+
async function requestCommandApproval(command) {
|
|
181
|
+
const mode = getApprovalMode();
|
|
182
|
+
if (mode === 'skip-all') {
|
|
183
|
+
p.log.info(`${pc.dim('Auto-approved (skip-all):')} ${pc.yellow(command)}`);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (mode === 'skip-once') {
|
|
187
|
+
p.log.info(`${pc.dim('Auto-approved (skip-once):')} ${pc.yellow(command)}`);
|
|
188
|
+
consumeSkipOnce();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
// mode === 'ask'
|
|
192
|
+
const result = await p.select({
|
|
193
|
+
message: `Approve command: ${pc.yellow(command)}`,
|
|
194
|
+
options: [
|
|
195
|
+
{ value: 'approve', label: 'Yes, run this command' },
|
|
196
|
+
{ value: 'skip-once', label: 'Yes, and skip approval for the next command too' },
|
|
197
|
+
{
|
|
198
|
+
value: 'skip-all',
|
|
199
|
+
label: 'Yes, auto-approve all future commands (Note: this lasts until you restart the CLI)',
|
|
200
|
+
},
|
|
201
|
+
{ value: 'deny', label: 'No, deny this command' },
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
if (p.isCancel(result) || result === 'deny') {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (result === 'skip-once') {
|
|
208
|
+
setApprovalMode('skip-once');
|
|
209
|
+
}
|
|
210
|
+
else if (result === 'skip-all') {
|
|
211
|
+
setApprovalMode('skip-all');
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
// ─── Agent Loop ──────────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Processes a single model response that may contain tool calls.
|
|
218
|
+
* Handles the full tool-call loop: execute → feed results → repeat
|
|
219
|
+
* until the model produces a final text response.
|
|
220
|
+
*/
|
|
221
|
+
async function processResponse(chat, result, workspaceRoot, inputHandler, agentState, abortSignal) {
|
|
222
|
+
let response = result.response;
|
|
223
|
+
let turnCount = 0;
|
|
224
|
+
const MAX_TURNS = 25;
|
|
225
|
+
// Loop while the model keeps requesting tool calls
|
|
226
|
+
while (true) {
|
|
227
|
+
turnCount++;
|
|
228
|
+
if (turnCount > MAX_TURNS) {
|
|
229
|
+
p.log.error(`${pc.red('System Error:')} Agent exceeded maximum autonomous turns (${MAX_TURNS}). Force stopping to prevent infinite loop.`);
|
|
230
|
+
return 'I have exceeded the maximum allowed number of autonomous actions (25 turns) and was force-stopped by the system to prevent an infinite loop. Please adjust your request or guide me on what went wrong.';
|
|
231
|
+
}
|
|
232
|
+
const functionCalls = response.functionCalls();
|
|
233
|
+
if (!functionCalls || functionCalls.length === 0) {
|
|
234
|
+
// No more tool calls — return the final text response
|
|
235
|
+
return response.text() ?? '';
|
|
236
|
+
}
|
|
237
|
+
// Process each tool call
|
|
238
|
+
const toolResponses = [];
|
|
239
|
+
for (const fc of functionCalls) {
|
|
240
|
+
const toolName = fc.name;
|
|
241
|
+
const toolArgs = (fc.args ?? {});
|
|
242
|
+
// Display what the agent is doing
|
|
243
|
+
p.log.step(formatToolCall(toolName, toolArgs));
|
|
244
|
+
// For run_command, request user approval
|
|
245
|
+
if (toolName === 'run_command') {
|
|
246
|
+
await inputHandler.waitForPrompt();
|
|
247
|
+
inputHandler.stop();
|
|
248
|
+
const approved = await requestCommandApproval(toolArgs.command);
|
|
249
|
+
inputHandler.start();
|
|
250
|
+
if (!approved) {
|
|
251
|
+
toolResponses.push({
|
|
252
|
+
functionResponse: {
|
|
253
|
+
name: toolName,
|
|
254
|
+
response: {
|
|
255
|
+
output: '',
|
|
256
|
+
error: 'Command was denied by the user. Do not retry this command. Ask the user how they would like to proceed.',
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Execute the tool
|
|
264
|
+
const toolResult = await executeTool(workspaceRoot, toolName, toolArgs);
|
|
265
|
+
if (toolResult.error) {
|
|
266
|
+
debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
|
|
267
|
+
// Truncate the error message for the terminal UI to prevent console clutter
|
|
268
|
+
// (e.g. hiding the large file previews sent to the AI)
|
|
269
|
+
const displayError = toolResult.error.split('\n')[0].substring(0, 100);
|
|
270
|
+
p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
|
|
271
|
+
}
|
|
272
|
+
toolResponses.push({
|
|
273
|
+
functionResponse: {
|
|
274
|
+
name: toolName,
|
|
275
|
+
response: {
|
|
276
|
+
output: toolResult.output,
|
|
277
|
+
...(toolResult.error ? { error: toolResult.error } : {}),
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
await inputHandler.waitForPrompt();
|
|
283
|
+
const queuedMsg = inputHandler.getAndClear();
|
|
284
|
+
let additionalText = undefined;
|
|
285
|
+
if (queuedMsg) {
|
|
286
|
+
additionalText = `[USER INTERRUPTION] The user sent the following message during your execution:\n"${queuedMsg}"\n\nPlease incorporate this feedback into your ongoing work. Address the user's message, but DO NOT lose track of your original overall plan or focus. After addressing this interruption, continue with your broader objective.`;
|
|
287
|
+
p.log.info(pc.cyan(`Sending queued message to AI...`));
|
|
288
|
+
const newIntent = await routeIntent(queuedMsg);
|
|
289
|
+
if (agentState.targetAgent === 'CHAT') {
|
|
290
|
+
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
291
|
+
agentState.targetAgent = 'EXECUTE';
|
|
292
|
+
const config = getPlanExecutionConfig();
|
|
293
|
+
chat.setAgentConfig(config.systemInstruction, config.tools);
|
|
294
|
+
p.log.info(pc.yellow(`Upgraded session intent to EXECUTE based on chained message.`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Feed tool results back to the model
|
|
299
|
+
let followUp;
|
|
300
|
+
try {
|
|
301
|
+
followUp = await chat.sendMessage(toolResponses, additionalText, abortSignal);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
305
|
+
p.log.warn(pc.yellow('Generation stopped by user.'));
|
|
306
|
+
return '[Generation stopped by user]';
|
|
307
|
+
}
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
const grounding = followUp.response.groundingMetadata?.();
|
|
311
|
+
if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
|
312
|
+
p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
|
|
313
|
+
}
|
|
314
|
+
response = followUp.response;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Starts the interactive agent chat loop.
|
|
319
|
+
* Runs until the user types "exit", "quit", or presses Ctrl+C.
|
|
320
|
+
*/
|
|
321
|
+
export async function startAgentLoop(workspaceRoot, version) {
|
|
322
|
+
const chat = createSharedChatSession();
|
|
323
|
+
const inputHandler = new AsyncInputHandler();
|
|
324
|
+
// Intercept terminal paste events to prevent accidental early-submission of multi-line pastes
|
|
325
|
+
const originalEmit = process.stdin.emit.bind(process.stdin);
|
|
326
|
+
process.stdin.emit = function (event, ...args) {
|
|
327
|
+
if (event === 'data' && Buffer.isBuffer(args[0])) {
|
|
328
|
+
let chunk = args[0].toString();
|
|
329
|
+
// If a single data chunk is longer than 2 characters and contains a newline, it is a paste event.
|
|
330
|
+
// (Normal typing sends 1 character per data event. Enter key sends exactly 1 character).
|
|
331
|
+
if (chunk.length > 2 && (chunk.includes('\n') || chunk.includes('\r'))) {
|
|
332
|
+
chunk = chunk.replace(/\r?\n/g, ' ');
|
|
333
|
+
args[0] = Buffer.from(chunk);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return originalEmit(event, ...args);
|
|
337
|
+
};
|
|
338
|
+
console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
|
|
339
|
+
while (true) {
|
|
340
|
+
inputHandler.stop();
|
|
341
|
+
let lines = [];
|
|
342
|
+
let isMultiLine = false;
|
|
343
|
+
let canceledGlobal = false;
|
|
344
|
+
while (true) {
|
|
345
|
+
const userInputRaw = await p.text({
|
|
346
|
+
message: isMultiLine ? ' ' : '❯',
|
|
347
|
+
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
348
|
+
});
|
|
349
|
+
if (p.isCancel(userInputRaw)) {
|
|
350
|
+
if (isMultiLine) {
|
|
351
|
+
p.log.warn(pc.yellow('Multi-line input canceled.'));
|
|
352
|
+
lines = [];
|
|
353
|
+
isMultiLine = false;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
canceledGlobal = true;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
let line = (userInputRaw || '');
|
|
362
|
+
if (line.trimEnd().endsWith('\\')) {
|
|
363
|
+
const cleanLine = line.trimEnd().slice(0, -1);
|
|
364
|
+
lines.push(cleanLine);
|
|
365
|
+
isMultiLine = true;
|
|
366
|
+
// Clack's text prompt dims the submitted value, which can be completely invisible on some terminal themes.
|
|
367
|
+
// We explicitly log the line here so the user can see what they are typing in multiline mode.
|
|
368
|
+
p.log.step(pc.cyan(cleanLine));
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
lines.push(line);
|
|
372
|
+
if (isMultiLine) {
|
|
373
|
+
p.log.step(pc.cyan(line));
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (canceledGlobal) {
|
|
379
|
+
p.outro(pc.green('Goodbye! Happy coding. 🚀'));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let userInput = lines.join('\n').trim();
|
|
383
|
+
if (!userInput) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (userInput === '/') {
|
|
387
|
+
const commandMenu = await p.select({
|
|
388
|
+
message: 'Command Menu',
|
|
389
|
+
options: [
|
|
390
|
+
{ value: '/models', label: '/models', hint: 'Change the active AI model' },
|
|
391
|
+
{ value: '/clear', label: '/clear', hint: 'Clear chat session history' },
|
|
392
|
+
{ value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
|
|
393
|
+
{ value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
|
|
394
|
+
{ value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
|
|
395
|
+
{ value: '/revert', label: '/revert', hint: 'Undo last change' },
|
|
396
|
+
{ value: 'exit', label: 'exit', hint: 'Close the CLI' },
|
|
397
|
+
{ value: 'cancel', label: 'cancel', hint: 'Return to chat' },
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
if (p.isCancel(commandMenu) || commandMenu === 'cancel') {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
userInput = commandMenu;
|
|
404
|
+
}
|
|
405
|
+
if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
|
|
406
|
+
p.outro(pc.green('Goodbye! Happy coding. 🚀'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (userInput.length === 0) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Handle special commands
|
|
413
|
+
if (userInput.toLowerCase() === '/clear') {
|
|
414
|
+
chat.clearHistory();
|
|
415
|
+
process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
|
|
416
|
+
p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + version)}`);
|
|
417
|
+
p.log.info(`${pc.dim('Workspace:')} ${pc.cyan(workspaceRoot)}`);
|
|
418
|
+
p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu. Type ${pc.yellow('exit')} to leave.`);
|
|
419
|
+
p.log.success('Chat history cleared.');
|
|
420
|
+
console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (userInput.toLowerCase() === '/models') {
|
|
424
|
+
const selectedModel = await p.select({
|
|
425
|
+
message: 'Select AI Model',
|
|
426
|
+
options: [
|
|
427
|
+
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'Best for complex coding & large context' },
|
|
428
|
+
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'Balanced performance' },
|
|
429
|
+
// {value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'Anthropic: Highly capable, complex reasoning'},
|
|
430
|
+
// {value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', hint: 'Anthropic: Fast and highly intelligent'},
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
if (!p.isCancel(selectedModel)) {
|
|
434
|
+
chat.setModel(selectedModel);
|
|
435
|
+
p.log.success(`Model successfully switched to ${pc.cyan(selectedModel)}`);
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (userInput.toLowerCase() === '/debug') {
|
|
440
|
+
const isEnabled = toggleDebugMode();
|
|
441
|
+
if (isEnabled) {
|
|
442
|
+
p.log.success('Debug mode enabled. Internal logs will now be shown.');
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
p.log.success('Debug mode disabled.');
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (userInput.toLowerCase() === '/auto-approve') {
|
|
450
|
+
setApprovalMode('skip-all');
|
|
451
|
+
p.log.success('Auto-approve enabled for all future commands in this session.');
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (userInput.toLowerCase() === '/revert') {
|
|
455
|
+
const lastChangeSet = changeLogger.popLastChangeSet();
|
|
456
|
+
if (!lastChangeSet) {
|
|
457
|
+
p.log.warn('No changes to revert.');
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const spinner = p.spinner();
|
|
461
|
+
spinner.start('Reverting last changes...');
|
|
462
|
+
try {
|
|
463
|
+
for (const change of lastChangeSet.changes) {
|
|
464
|
+
const absPath = path.resolve(workspaceRoot, change.filePath);
|
|
465
|
+
if (change.action === 'create') {
|
|
466
|
+
await fs.rm(absPath, { force: true });
|
|
467
|
+
}
|
|
468
|
+
else if (change.originalContent !== null) {
|
|
469
|
+
await fs.writeFile(absPath, change.originalContent, 'utf-8');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
spinner.stop('Reverted successfully.');
|
|
473
|
+
p.log.success(`Reverted changes for: ${lastChangeSet.description}`);
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
spinner.stop('Revert failed.');
|
|
477
|
+
p.log.error(`Failed to revert: ${err instanceof Error ? err.message : String(err)}`);
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (userInput.toLowerCase() === '/commit') {
|
|
482
|
+
const commitSpinner = p.spinner();
|
|
483
|
+
commitSpinner.start('Staging changes and analyzing diff...');
|
|
484
|
+
try {
|
|
485
|
+
await execAsync('git add .', { cwd: workspaceRoot });
|
|
486
|
+
const { stdout: diffOut } = await execAsync('git diff --cached', { cwd: workspaceRoot });
|
|
487
|
+
if (!diffOut || diffOut.trim().length === 0) {
|
|
488
|
+
commitSpinner.stop('No changes to commit.');
|
|
489
|
+
p.log.warn('Git working tree is clean.');
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
commitSpinner.message('Generating commit message...');
|
|
493
|
+
const prompt = `Generate a concise, standard git commit message for the following diff. Only return the commit message text.
|
|
494
|
+
|
|
495
|
+
<diff>
|
|
496
|
+
${diffOut}
|
|
497
|
+
</diff>`;
|
|
498
|
+
// Use flash-lite for extreme speed and low cost for this simple task
|
|
499
|
+
const commitSystemPrompt = 'You are an expert developer. Output only the git commit message, no markdown formatting, no explanations. Follow Conventional Commits format (feat:, fix:, chore:, refactor:, etc.) for the first line, keeping it under 70 characters and using the imperative mood. Then, add a blank line followed by a more descriptive bulleted list explaining the "what" and "why" of the changes based on the diff.';
|
|
500
|
+
const commitAgent = new ProxyChatSession('gemini-2.5-flash-lite', commitSystemPrompt, [], {});
|
|
501
|
+
const commitResult = await commitAgent.sendMessage(prompt);
|
|
502
|
+
const commitMsg = commitResult.response.text().trim();
|
|
503
|
+
commitSpinner.message('Committing...');
|
|
504
|
+
const tmpMsgPath = path.join(workspaceRoot, '.gemini-commit-msg.tmp');
|
|
505
|
+
await fs.writeFile(tmpMsgPath, commitMsg, 'utf-8');
|
|
506
|
+
try {
|
|
507
|
+
await execAsync(`git commit -F .gemini-commit-msg.tmp`, { cwd: workspaceRoot });
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
await fs.rm(tmpMsgPath, { force: true });
|
|
511
|
+
}
|
|
512
|
+
commitSpinner.stop('Committed successfully.');
|
|
513
|
+
p.log.success(`Committed with message:\n\n${pc.dim(commitMsg)}`);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
commitSpinner.stop('Commit failed.');
|
|
517
|
+
p.log.error(`Failed to commit: ${err instanceof Error ? err.message : String(err)}`);
|
|
518
|
+
}
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const spinner = p.spinner();
|
|
522
|
+
const ac = new AbortController();
|
|
523
|
+
inputHandler.setAbortController(ac);
|
|
524
|
+
try {
|
|
525
|
+
inputHandler.start(spinner);
|
|
526
|
+
changeLogger.startChangeSet(userInput);
|
|
527
|
+
let finalInput = userInput;
|
|
528
|
+
spinner.start('🔍 Investigating workspace...');
|
|
529
|
+
const chatHistory = chat.getRecentHistory(3);
|
|
530
|
+
const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
|
|
531
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
532
|
+
spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
let latestUsage = undefined;
|
|
536
|
+
// Also grab any messages queued exactly between gatherContext ending and here
|
|
537
|
+
const leftoverMsg = inputHandler.getAndClear();
|
|
538
|
+
if (leftoverMsg) {
|
|
539
|
+
gatherRes.chainedMessages.push(leftoverMsg);
|
|
540
|
+
}
|
|
541
|
+
let effectiveTargetAgent = gatherRes.targetAgent;
|
|
542
|
+
if (gatherRes.chainedMessages.length > 0) {
|
|
543
|
+
const chainedContent = gatherRes.chainedMessages.join('\n');
|
|
544
|
+
const newIntent = await routeIntent(chainedContent);
|
|
545
|
+
if (gatherRes.contextResult !== null) {
|
|
546
|
+
// Was SEARCH: Can change to EXECUTE or CHAT based on chained message
|
|
547
|
+
effectiveTargetAgent = newIntent.targetAgent;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
// Was NOT SEARCH
|
|
551
|
+
if (gatherRes.targetAgent === 'EXECUTE') {
|
|
552
|
+
// Executing cannot downgrade to CHAT
|
|
553
|
+
effectiveTargetAgent = 'EXECUTE';
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
// Was CHAT: Can change to SEARCH or EXECUTE. Both are treated as EXECUTE to grant tools.
|
|
557
|
+
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
558
|
+
effectiveTargetAgent = 'EXECUTE';
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
finalInput += `\n\n[USER FOLLOW-UP INSTRUCTIONS SENT DURING INVESTIGATION]:\n${chainedContent}\n\nPlease incorporate these instructions into your work. Address them appropriately, but ensure you do not lose track of the original request's primary objective.`;
|
|
563
|
+
}
|
|
564
|
+
// Hot-swap the agent configuration based on intent
|
|
565
|
+
debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
|
|
566
|
+
const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
|
|
567
|
+
let dynamicSystemInstruction = config.systemInstruction;
|
|
568
|
+
if (gatherRes.contextResult) {
|
|
569
|
+
const contextInjection = buildContextInjection(gatherRes.contextResult);
|
|
570
|
+
debugLog(`Context injection size: ${contextInjection.length} chars`);
|
|
571
|
+
dynamicSystemInstruction += '\n\n' + contextInjection;
|
|
572
|
+
}
|
|
573
|
+
chat.setAgentConfig(dynamicSystemInstruction, config.tools);
|
|
574
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
575
|
+
spinner.message('Thinking...');
|
|
576
|
+
}
|
|
577
|
+
let result;
|
|
578
|
+
try {
|
|
579
|
+
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
583
|
+
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
584
|
+
spinner.stop('');
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
throw e;
|
|
588
|
+
}
|
|
589
|
+
latestUsage = result.response.usageMetadata?.();
|
|
590
|
+
const grounding = result.response.groundingMetadata?.();
|
|
591
|
+
if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
|
592
|
+
p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
|
|
593
|
+
}
|
|
594
|
+
spinner.stop('');
|
|
595
|
+
let finalText = '';
|
|
596
|
+
const MAX_CORRECTIONS = 3;
|
|
597
|
+
let correctionAttempts = 0;
|
|
598
|
+
const calls = result.response.functionCalls();
|
|
599
|
+
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
600
|
+
let agentState = { targetAgent: effectiveTargetAgent };
|
|
601
|
+
while (correctionAttempts <= MAX_CORRECTIONS) {
|
|
602
|
+
if (finalText === '[Generation stopped by user]')
|
|
603
|
+
break;
|
|
604
|
+
finalText = await processResponse(chat, result, workspaceRoot, inputHandler, agentState, ac.signal);
|
|
605
|
+
debugLog(`processResponse returned finalText (length ${finalText.length}): "${finalText.substring(0, 50)}..."`);
|
|
606
|
+
const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
|
|
607
|
+
const changedFiles = currentChanges
|
|
608
|
+
.filter((c) => c.action === 'create' || c.action === 'modify')
|
|
609
|
+
.map((c) => c.filePath);
|
|
610
|
+
if (changedFiles.length === 0)
|
|
611
|
+
break;
|
|
612
|
+
p.log.step('Verifying modified files...');
|
|
613
|
+
const verificationErrors = await verifyChangedFiles(workspaceRoot, changedFiles);
|
|
614
|
+
if (!verificationErrors) {
|
|
615
|
+
p.log.success('Verification passed.');
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
correctionAttempts++;
|
|
619
|
+
if (correctionAttempts > MAX_CORRECTIONS) {
|
|
620
|
+
p.log.warn('Max self-correction attempts reached. Leaving remaining errors for manual review.');
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
p.log.warn(`${pc.yellow('Verification failed. Auto-correcting errors')} (Attempt ${correctionAttempts}/${MAX_CORRECTIONS})...`);
|
|
624
|
+
// Optionally show a snippet of the error to the user so they aren't left in the dark
|
|
625
|
+
const displayError = verificationErrors.split('\n').slice(0, 5).join('\n');
|
|
626
|
+
console.log(pc.dim(` ${displayError.replace(/\n/g, '\n ')}\n ...`));
|
|
627
|
+
debugLog(`Verification failed on attempt ${correctionAttempts}/${MAX_CORRECTIONS}. Errors:\n${verificationErrors}`);
|
|
628
|
+
const correctionPrompt = `AUTOMATED SYSTEM CHECK: Your previous changes resulted in the following errors:\n\n${verificationErrors}\n\nPlease analyze these errors and use your file modification tools to fix them.`;
|
|
629
|
+
spinner.start('Thinking (Correction)...');
|
|
630
|
+
result = await chat.sendMessage(correctionPrompt);
|
|
631
|
+
spinner.stop('');
|
|
632
|
+
}
|
|
633
|
+
changeLogger.commitChangeSet();
|
|
634
|
+
if (finalText) {
|
|
635
|
+
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
|
|
636
|
+
console.log(marked.parse(finalText));
|
|
637
|
+
}
|
|
638
|
+
if (latestUsage && latestUsage.remainingBalance !== undefined) {
|
|
639
|
+
p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
spinner.stop('');
|
|
644
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
645
|
+
p.log.error(`${pc.red('Error:')} ${message}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|