minovative-mind-cli 1.0.5 → 1.1.1
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/dist/commands/chat.js +6 -1
- package/dist/services/agent-tools.d.ts +3 -3
- package/dist/services/agent-tools.js +16 -6
- package/dist/services/agent.d.ts +124 -0
- package/dist/services/agent.js +316 -44
- package/dist/services/ai.d.ts +7 -2
- package/dist/services/ai.js +36 -17
- package/dist/services/proxyClient.js +6 -1
- package/dist/services/verificationService.js +7 -41
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/logger.d.ts +2 -6
- package/dist/utils/logger.js +12 -8
- package/dist/utils/systemPrompts.d.ts +5 -5
- package/dist/utils/systemPrompts.js +72 -33
- package/oclif.manifest.json +1 -1
- package/package.json +4 -2
package/dist/services/agent.js
CHANGED
|
@@ -1,36 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core agent execution service.
|
|
3
|
+
*
|
|
4
|
+
* This file coordinates the central execution environment for Minovative Mind,
|
|
5
|
+
* an AI-powered developer CLI. It orchestrates:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Interactive REPL Loop**: Collects multiline user input, supports slash-commands,
|
|
8
|
+
* reverting changes, switching models, auto-commit, and clipboard paste injection.
|
|
9
|
+
* 2. **Context-Aware Investigation**: Gathers repo-wide context and compresses it using
|
|
10
|
+
* low-cost, high-speed LLM summarization.
|
|
11
|
+
* 3. **Autonomous Tool Loop (processResponse)**: Allows the AI model to execute file-system,
|
|
12
|
+
* shell-command, and search operations, feeding outcomes back in a recursive, multi-turn loop.
|
|
13
|
+
* 4. **Asynchronous Interrupt Handling (AsyncInputHandler)**: Intercepts keyboard events
|
|
14
|
+
* on standard input to let users pause execution, queue live feedback, or abort generation.
|
|
15
|
+
* 5. **Self-Correction Pipeline**: Compares modified files against TS/Linter diagnostics,
|
|
16
|
+
* providing feedback on syntax or semantic failures back to the AI for self-healing.
|
|
17
|
+
*/
|
|
1
18
|
import * as p from '@clack/prompts';
|
|
2
19
|
import pc from 'picocolors';
|
|
3
20
|
import { promises as fs } from 'node:fs';
|
|
4
21
|
import path from 'node:path';
|
|
5
22
|
import { exec } from 'node:child_process';
|
|
6
23
|
import { promisify } from 'node:util';
|
|
24
|
+
import { debugLog, toggleDebugMode } from '../utils/logger.js';
|
|
7
25
|
const execAsync = promisify(exec);
|
|
26
|
+
/**
|
|
27
|
+
* Asynchronous Input Handler (AsyncInputHandler)
|
|
28
|
+
*
|
|
29
|
+
* This class intercepts user keyboard input from standard input (stdin) while
|
|
30
|
+
* background processes (e.g., Gemini model generations or long shell tool runs)
|
|
31
|
+
* are executing. It provides seamless execution management by letting users:
|
|
32
|
+
*
|
|
33
|
+
* 1. **Pause execution** at any time by pressing a key.
|
|
34
|
+
* 2. **Queue feedback** ("chained messages") without waiting for the entire run to finish.
|
|
35
|
+
* 3. **Force abort** the current model or tool run by entering "stop" or "stop!".
|
|
36
|
+
*
|
|
37
|
+
* ### Raw Mode & Terminal States
|
|
38
|
+
* In normal CLI execution, Node.js waits for a line feed (Enter) before emitting input.
|
|
39
|
+
* To intercept immediate keystrokes, we put `process.stdin` into *raw mode*.
|
|
40
|
+
* While in raw mode, we listen for direct data buffers.
|
|
41
|
+
* To avoid visual conflicts with our logging output and Clack's CLI spinners,
|
|
42
|
+
* we dynamically pause spinners, detach listeners, disable raw mode, open standard
|
|
43
|
+
* interactive text-prompt forms, and resume raw mode and spinners upon completion.
|
|
44
|
+
*/
|
|
8
45
|
export class AsyncInputHandler {
|
|
46
|
+
/** Queue of pending user feedback/instructions typed during active background execution */
|
|
9
47
|
queue = [];
|
|
48
|
+
/** Guard flag preventing multiple simultaneous input prompt overlays */
|
|
10
49
|
isPrompting = false;
|
|
50
|
+
/** Reference to the Clack CLI spinner which must be paused/restarted during prompts */
|
|
11
51
|
spinner = null;
|
|
52
|
+
/** Stores the terminal's raw mode configuration state before handler activation */
|
|
12
53
|
originalRawMode = false;
|
|
54
|
+
/** Indicates whether the input handler is currently inactive/stopped */
|
|
13
55
|
stopped = true;
|
|
56
|
+
/** Reference to the AbortController controlling the active AI request to trigger cancellations */
|
|
14
57
|
ac = null;
|
|
58
|
+
/**
|
|
59
|
+
* Registers the active AbortController for the current AI request.
|
|
60
|
+
* This is triggered when the user commands a process cancel (e.g., typing "stop").
|
|
61
|
+
*
|
|
62
|
+
* @param ac - The AbortController controlling the current generation.
|
|
63
|
+
*/
|
|
15
64
|
setAbortController(ac) {
|
|
16
65
|
this.ac = ac;
|
|
17
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Determines if a user-prompt dialog is actively running.
|
|
69
|
+
* Useful for coordinating other console logging output to avoid UI overlap.
|
|
70
|
+
*
|
|
71
|
+
* @returns True if a text prompt is currently displayed, false otherwise.
|
|
72
|
+
*/
|
|
18
73
|
isCurrentlyPrompting() {
|
|
19
74
|
return this.isPrompting;
|
|
20
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Blocks and waits until any active prompting action is completed.
|
|
78
|
+
* Guarantees terminal stdout is clean before resuming logs.
|
|
79
|
+
*/
|
|
21
80
|
async waitForPrompt() {
|
|
22
81
|
while (this.isPrompting) {
|
|
23
82
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
83
|
}
|
|
25
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Core stdin event listener. Detects pressed keys, pauses background visual elements,
|
|
87
|
+
* handles exit interrupts, and opens a Clack text dialog for input.
|
|
88
|
+
*
|
|
89
|
+
* Handles recovery of the standard input stream and raw mode states even if
|
|
90
|
+
* errors occur during prompt initialization.
|
|
91
|
+
*
|
|
92
|
+
* @param chunk - The raw terminal buffer containing keypress data.
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
26
95
|
onData = async (chunk) => {
|
|
27
96
|
try {
|
|
28
97
|
if (this.isPrompting)
|
|
29
98
|
return;
|
|
30
99
|
const char = chunk.toString();
|
|
100
|
+
// Handle Ctrl+C (End of Text ASCII 0x03) immediately
|
|
31
101
|
if (char === '\u0003') {
|
|
32
102
|
process.exit(0);
|
|
33
103
|
}
|
|
104
|
+
// Ignore standard non-printable control keys, escape sequences, etc.
|
|
34
105
|
if (char.charCodeAt(0) < 32 || char === '\u007f')
|
|
35
106
|
return;
|
|
36
107
|
this.isPrompting = true;
|
|
@@ -38,9 +109,12 @@ export class AsyncInputHandler {
|
|
|
38
109
|
if (process.stdin.isTTY) {
|
|
39
110
|
process.stdin.setRawMode(false);
|
|
40
111
|
}
|
|
112
|
+
// Hide the background spinner before prompt output to avoid corrupting terminal lines
|
|
41
113
|
if (this.spinner) {
|
|
42
114
|
this.spinner.stop('Paused to receive input');
|
|
43
115
|
}
|
|
116
|
+
// Capture the user feedback. The character typed to trigger this event is passed
|
|
117
|
+
// as the initial value of the prompt to avoid losing the first keystroke.
|
|
44
118
|
const userInput = await p.text({
|
|
45
119
|
message: 'Add chained message:',
|
|
46
120
|
placeholder: '(Leave blank and press Enter to cancel)',
|
|
@@ -64,13 +138,14 @@ export class AsyncInputHandler {
|
|
|
64
138
|
}
|
|
65
139
|
}
|
|
66
140
|
catch (err) {
|
|
67
|
-
//
|
|
141
|
+
// Absorb and suppress errors safely during raw stream intercepts
|
|
68
142
|
}
|
|
69
143
|
finally {
|
|
70
144
|
if (!this.stopped) {
|
|
71
145
|
if (process.stdin.isTTY) {
|
|
72
146
|
process.stdin.setRawMode(true);
|
|
73
147
|
}
|
|
148
|
+
// Small delay before reattaching listener to avoid capturing duplicate keypress frames
|
|
74
149
|
setTimeout(() => {
|
|
75
150
|
if (!this.stopped) {
|
|
76
151
|
process.stdin.on('data', this.onData);
|
|
@@ -83,6 +158,12 @@ export class AsyncInputHandler {
|
|
|
83
158
|
}
|
|
84
159
|
}
|
|
85
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Starts intercepting keystrokes and enables raw terminal processing.
|
|
163
|
+
* Saves the original raw mode configuration to ensure a clean restoration later.
|
|
164
|
+
*
|
|
165
|
+
* @param spinner - The current active Clack spinner UI reference, if any.
|
|
166
|
+
*/
|
|
86
167
|
start(spinner) {
|
|
87
168
|
this.stopped = false;
|
|
88
169
|
if (spinner) {
|
|
@@ -95,6 +176,10 @@ export class AsyncInputHandler {
|
|
|
95
176
|
process.stdin.resume();
|
|
96
177
|
process.stdin.on('data', this.onData);
|
|
97
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Disables raw mode, stops intercepting keystrokes, and restores
|
|
181
|
+
* the terminal stdin stream to its original raw/cooked state.
|
|
182
|
+
*/
|
|
98
183
|
stop() {
|
|
99
184
|
this.stopped = true;
|
|
100
185
|
process.stdin.removeListener('data', this.onData);
|
|
@@ -103,6 +188,12 @@ export class AsyncInputHandler {
|
|
|
103
188
|
}
|
|
104
189
|
this.spinner = null;
|
|
105
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Retrieves and flushes all user feedback messages accumulated during execution.
|
|
193
|
+
*
|
|
194
|
+
* @returns A concatenated string of all queued user messages separated by newlines,
|
|
195
|
+
* or an empty string if nothing was queued.
|
|
196
|
+
*/
|
|
106
197
|
getAndClear() {
|
|
107
198
|
if (this.queue.length === 0)
|
|
108
199
|
return '';
|
|
@@ -112,17 +203,20 @@ export class AsyncInputHandler {
|
|
|
112
203
|
}
|
|
113
204
|
}
|
|
114
205
|
import { consumeSkipOnce, executeTool, getApprovalMode, setApprovalMode } from './agent-tools.js';
|
|
115
|
-
import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession } from './ai.js';
|
|
206
|
+
import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession, compressTextUsingFlashLite, } from './ai.js';
|
|
116
207
|
import { changeLogger } from './changeLogger.js';
|
|
117
208
|
import { gatherContext, routeIntent } from './contextAgent.js';
|
|
118
209
|
import { verifyChangedFiles } from './verificationService.js';
|
|
119
210
|
import { buildContextInjection } from '../utils/contextPrompts.js';
|
|
120
|
-
import { debugLog, toggleDebugMode } from '../utils/logger.js';
|
|
121
211
|
import { readPaste } from '../utils/paste.js';
|
|
122
212
|
import { marked } from 'marked';
|
|
123
213
|
import { markedTerminal } from 'marked-terminal';
|
|
124
214
|
marked.use(markedTerminal());
|
|
125
215
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Mapping of tool identifiers to user-friendly terminal emojis.
|
|
218
|
+
* Enhances the visual feedback during background tool execution turns.
|
|
219
|
+
*/
|
|
126
220
|
const TOOL_ICONS = {
|
|
127
221
|
read_file: '📖',
|
|
128
222
|
write_file: '✏️',
|
|
@@ -134,6 +228,10 @@ const TOOL_ICONS = {
|
|
|
134
228
|
rename_file: '🚚',
|
|
135
229
|
find_dependencies: '🔗',
|
|
136
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Human-readable translations for tool actions.
|
|
233
|
+
* Used for constructing clear, active-verb descriptive log headers in the CLI.
|
|
234
|
+
*/
|
|
137
235
|
const TOOL_LABELS = {
|
|
138
236
|
read_file: 'Reading file',
|
|
139
237
|
write_file: 'Writing file',
|
|
@@ -146,12 +244,33 @@ const TOOL_LABELS = {
|
|
|
146
244
|
find_dependencies: 'Tracing dependencies',
|
|
147
245
|
};
|
|
148
246
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
247
|
+
/**
|
|
248
|
+
* Formats a tool execution call into a beautifully stylized console status line.
|
|
249
|
+
* Extracts context-specific arguments to display relevant parameters in real-time.
|
|
250
|
+
*
|
|
251
|
+
* E.g., for `read_file`, it outputs line-range constraints. For `rename_file`, it prints
|
|
252
|
+
* a path redirection arrow.
|
|
253
|
+
*
|
|
254
|
+
* @param name - The identifier of the tool being called (e.g. 'read_file').
|
|
255
|
+
* @param args - The dictionary of parameters supplied to the tool.
|
|
256
|
+
* @returns A fully colorized and formatted ANSI console string.
|
|
257
|
+
*/
|
|
149
258
|
function formatToolCall(name, args) {
|
|
150
259
|
const icon = TOOL_ICONS[name] ?? '🔧';
|
|
151
260
|
const label = TOOL_LABELS[name] ?? name;
|
|
152
261
|
switch (name) {
|
|
153
|
-
case 'read_file':
|
|
154
|
-
|
|
262
|
+
case 'read_file': {
|
|
263
|
+
let extra = '';
|
|
264
|
+
if (args.startLine !== undefined || args.endLine !== undefined) {
|
|
265
|
+
const start = args.startLine ?? 1;
|
|
266
|
+
const end = args.endLine ?? 'end';
|
|
267
|
+
extra = pc.dim(` (Lines ${start}-${end})`);
|
|
268
|
+
}
|
|
269
|
+
else if (Array.isArray(args.targetElements) && args.targetElements.length > 0) {
|
|
270
|
+
extra = pc.dim(` (Elements: ${args.targetElements.join(', ')})`);
|
|
271
|
+
}
|
|
272
|
+
return `${icon} ${label}${extra}: ${pc.cyan(String(args.filePath))}`;
|
|
273
|
+
}
|
|
155
274
|
case 'write_file':
|
|
156
275
|
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
|
|
157
276
|
case 'modify_file':
|
|
@@ -173,10 +292,16 @@ function formatToolCall(name, args) {
|
|
|
173
292
|
}
|
|
174
293
|
}
|
|
175
294
|
/**
|
|
176
|
-
* Prompts the user to approve a shell command
|
|
177
|
-
*
|
|
295
|
+
* Prompts the user to approve a pending shell command requested by the AI.
|
|
296
|
+
* Implements a safety gate to protect the host machine's environment from arbitrary code execution.
|
|
178
297
|
*
|
|
179
|
-
*
|
|
298
|
+
* Supports three approval models:
|
|
299
|
+
* 1. **Ask**: Prompts the user for every shell execution command.
|
|
300
|
+
* 2. **Skip-Once**: Automatically grants permission to the current command, then resets.
|
|
301
|
+
* 3. **Skip-All / Auto-Approve**: Grants permission to all future terminal commands in the current session.
|
|
302
|
+
*
|
|
303
|
+
* @param command - The terminal string requested for execution.
|
|
304
|
+
* @returns A promise resolving to `true` if approved, or `false` if denied/cancelled.
|
|
180
305
|
*/
|
|
181
306
|
async function requestCommandApproval(command) {
|
|
182
307
|
const mode = getApprovalMode();
|
|
@@ -218,11 +343,49 @@ async function requestCommandApproval(command) {
|
|
|
218
343
|
* Processes a single model response that may contain tool calls.
|
|
219
344
|
* Handles the full tool-call loop: execute → feed results → repeat
|
|
220
345
|
* until the model produces a final text response.
|
|
346
|
+
*
|
|
347
|
+
* @param chat - The current active proxy chat session.
|
|
348
|
+
* @param result - The current result returned from sending a message to the model.
|
|
349
|
+
* @param workspaceRoot - The absolute/relative path to the workspace root.
|
|
350
|
+
* @param inputHandler - The input handler to check for queued interrupts.
|
|
351
|
+
* @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
|
|
352
|
+
* @param abortSignal - Signal to detect when the operation has been cancelled.
|
|
353
|
+
* @returns A promise that resolves to the final text response.
|
|
354
|
+
*/
|
|
355
|
+
/**
|
|
356
|
+
* Coordinates and executes the recursive model-response tool-execution loop.
|
|
357
|
+
*
|
|
358
|
+
* ### Execution Mechanism:
|
|
359
|
+
* This function processes the initial response from the generative model. If the model determines
|
|
360
|
+
* it needs to execute tools (e.g. read_file, run_command, modify_file) to answer or fulfill the request:
|
|
361
|
+
*
|
|
362
|
+
* 1. **Analyze Tool Requirements**: Parses requested `functionCalls` from the LLM.
|
|
363
|
+
* 2. **Check for User Abortion**: Periodically examines the `AbortSignal` to stop execution if requested.
|
|
364
|
+
* 3. **Manage Safety Approvals**: For high-risk operations (like `run_command`), requests explicit confirmation from the user (or uses auto-approve configurations).
|
|
365
|
+
* 4. **Execute Operations**: Runs requested tool actions via `executeTool` in parallel or series as requested.
|
|
366
|
+
* 5. **Handle Interrupts**: Integrates background user inputs queued in `inputHandler` as new context prompts back into the active LLM context.
|
|
367
|
+
* 6. **Submit Loop Frame**: Feeds execution outcomes back to the Gemini session and recursively repeats this sequence until the LLM produces a final text answer without further tool requests.
|
|
368
|
+
*
|
|
369
|
+
* ### Limits & Recovery Guardrails:
|
|
370
|
+
* - **Turn-Limiting**: Capped at `MAX_TURNS` (25) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
|
|
371
|
+
* - **Empty-Response Healing**: If the API returns an empty text response with no tools, the system initiates up to `MAX_EMPTY_RETRIES` (3) system-driven wake-up prompts to re-engage the model.
|
|
372
|
+
*
|
|
373
|
+
* @param chat - The current active proxy chat session.
|
|
374
|
+
* @param result - The current result returned from sending a message to the model.
|
|
375
|
+
* @param workspaceRoot - The absolute/relative path to the workspace root.
|
|
376
|
+
* @param inputHandler - The input handler to check for queued interrupts.
|
|
377
|
+
* @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
|
|
378
|
+
* @param abortSignal - Signal to detect when the operation has been cancelled.
|
|
379
|
+
* @returns A promise that resolves to the final text response.
|
|
221
380
|
*/
|
|
222
381
|
async function processResponse(chat, result, workspaceRoot, inputHandler, agentState, abortSignal) {
|
|
223
382
|
let response = result.response;
|
|
383
|
+
// Upper limit on autonomous sequential tool executions to prevent out-of-control loops
|
|
224
384
|
let turnCount = 0;
|
|
225
385
|
const MAX_TURNS = 25;
|
|
386
|
+
// Recovery thresholds for handling unexpected empty API payloads
|
|
387
|
+
let emptyRetryCount = 0;
|
|
388
|
+
const MAX_EMPTY_RETRIES = 3;
|
|
226
389
|
// Loop while the model keeps requesting tool calls
|
|
227
390
|
while (true) {
|
|
228
391
|
turnCount++;
|
|
@@ -233,21 +396,50 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
233
396
|
const functionCalls = response.functionCalls();
|
|
234
397
|
if (!functionCalls || functionCalls.length === 0) {
|
|
235
398
|
// No more tool calls — return the final text response
|
|
236
|
-
|
|
399
|
+
let finalOutput = response.text() ?? '';
|
|
400
|
+
if (!finalOutput.trim()) {
|
|
401
|
+
if (emptyRetryCount < MAX_EMPTY_RETRIES) {
|
|
402
|
+
emptyRetryCount++;
|
|
403
|
+
p.log.warn(pc.yellow(`AI returned an empty response. Attempting to recover (retry ${emptyRetryCount}/${MAX_EMPTY_RETRIES})...`));
|
|
404
|
+
// System-driven injection to kickstart the LLM's conversation generation
|
|
405
|
+
const wakeUpMessage = 'SYSTEM: You just returned a completely empty response with no tool calls. If you are stuck, please use your tools to explore the workspace, or explain what you are trying to do and ask the user for clarification. Do not return empty responses.';
|
|
406
|
+
try {
|
|
407
|
+
const retry = await chat.sendMessage(wakeUpMessage, undefined, abortSignal);
|
|
408
|
+
response = retry.response;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
413
|
+
p.log.warn(pc.yellow('Generation stopped by user during recovery.'));
|
|
414
|
+
return '[Generation stopped by user]';
|
|
415
|
+
}
|
|
416
|
+
finalOutput = '[System Error: The AI returned an empty response and failed to recover.]';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
finalOutput =
|
|
421
|
+
'[The AI repeatedly returned empty responses and could not recover. This usually means it hit an API filter/limit or is stuck. Try rewording your prompt.]';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return finalOutput;
|
|
237
425
|
}
|
|
238
|
-
// Process each tool call
|
|
426
|
+
// Process each tool call requested by the model
|
|
239
427
|
const toolResponses = [];
|
|
240
428
|
for (const fc of functionCalls) {
|
|
429
|
+
if (abortSignal.aborted) {
|
|
430
|
+
p.log.warn(pc.yellow('Execution aborted by user.'));
|
|
431
|
+
return '[Generation stopped by user]';
|
|
432
|
+
}
|
|
241
433
|
const toolName = fc.name;
|
|
242
434
|
const toolArgs = (fc.args ?? {});
|
|
243
|
-
//
|
|
435
|
+
// Log the ongoing tool action to the console
|
|
244
436
|
p.log.step(formatToolCall(toolName, toolArgs));
|
|
245
|
-
//
|
|
437
|
+
// If executing a CLI/shell command, wait for approval
|
|
246
438
|
if (toolName === 'run_command') {
|
|
247
439
|
await inputHandler.waitForPrompt();
|
|
248
|
-
inputHandler.stop();
|
|
440
|
+
inputHandler.stop(); // Temporarily release raw-mode during blocking interactive prompt
|
|
249
441
|
const approved = await requestCommandApproval(toolArgs.command);
|
|
250
|
-
inputHandler.start();
|
|
442
|
+
inputHandler.start(); // Re-engage raw-mode for continuous background monitoring
|
|
251
443
|
if (!approved) {
|
|
252
444
|
toolResponses.push({
|
|
253
445
|
functionResponse: {
|
|
@@ -261,14 +453,17 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
261
453
|
continue;
|
|
262
454
|
}
|
|
263
455
|
}
|
|
264
|
-
// Execute the tool
|
|
265
|
-
const toolResult = await executeTool(workspaceRoot, toolName, toolArgs);
|
|
456
|
+
// Execute the underlying filesystem, shell or search tool logic
|
|
457
|
+
const toolResult = await executeTool(workspaceRoot, toolName, toolArgs, abortSignal);
|
|
266
458
|
if (toolResult.error) {
|
|
267
459
|
debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
460
|
+
// Do not spam the user with expected chunking warnings
|
|
461
|
+
if (!toolResult.error.includes('File is too large')) {
|
|
462
|
+
// Truncate the error message for the terminal UI to prevent console clutter
|
|
463
|
+
// (e.g. hiding the large file previews sent to the AI)
|
|
464
|
+
const displayError = toolResult.error.split('\n')[0].substring(0, 100);
|
|
465
|
+
p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
|
|
466
|
+
}
|
|
272
467
|
}
|
|
273
468
|
toolResponses.push({
|
|
274
469
|
functionResponse: {
|
|
@@ -280,12 +475,14 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
280
475
|
},
|
|
281
476
|
});
|
|
282
477
|
}
|
|
478
|
+
// Check if the user entered any feedback or interrupt commands during the tool execution cycle
|
|
283
479
|
await inputHandler.waitForPrompt();
|
|
284
480
|
const queuedMsg = inputHandler.getAndClear();
|
|
285
481
|
let additionalText = undefined;
|
|
286
482
|
if (queuedMsg) {
|
|
287
483
|
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.`;
|
|
288
484
|
p.log.info(pc.cyan(`Sending queued message to AI...`));
|
|
485
|
+
// Dynamically upgrade agent permissions/intent to EXECUTE mode if the interrupted instruction requires system modifications
|
|
289
486
|
const newIntent = await routeIntent(queuedMsg);
|
|
290
487
|
if (agentState.targetAgent === 'CHAT') {
|
|
291
488
|
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
@@ -296,7 +493,7 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
296
493
|
}
|
|
297
494
|
}
|
|
298
495
|
}
|
|
299
|
-
// Feed tool results back to the model
|
|
496
|
+
// Feed tool results and potential interruption text back to the model
|
|
300
497
|
let followUp;
|
|
301
498
|
try {
|
|
302
499
|
followUp = await chat.sendMessage(toolResponses, additionalText, abortSignal);
|
|
@@ -318,12 +515,54 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
318
515
|
/**
|
|
319
516
|
* Starts the interactive agent chat loop.
|
|
320
517
|
* Runs until the user types "exit", "quit", or presses Ctrl+C.
|
|
518
|
+
*
|
|
519
|
+
* @param workspaceRoot - The root directory of the workspace.
|
|
520
|
+
* @param version - The active version of the CLI utility.
|
|
521
|
+
*/
|
|
522
|
+
/**
|
|
523
|
+
* Starts and orchestrates the primary interactive command-line interface (REPL) loop.
|
|
524
|
+
*
|
|
525
|
+
* This is the central control loop of the CLI application. It runs indefinitely until
|
|
526
|
+
* the user explicitly issues an exit directive (e.g., typing "exit", "quit", `/` command menu selections,
|
|
527
|
+
* or using keyboard breaks like Ctrl+C).
|
|
528
|
+
*
|
|
529
|
+
* ### Architectural Pipeline:
|
|
530
|
+
* 1. **User Input Collection**:
|
|
531
|
+
* - Captures multiline text strings by checking for trailing backslash characters (`\`).
|
|
532
|
+
* - Prevents visual terminal glitches during multi-line typing on varying themes.
|
|
533
|
+
* - Automatically handles command menu redirection when users enter a forward slash (`/`).
|
|
534
|
+
* - Intercepts and filters OS clipboard paste buffers to allow inserting massive scripts cleanly.
|
|
535
|
+
*
|
|
536
|
+
* 2. **Slash Commands Processing**:
|
|
537
|
+
* - `/paste`: Interactively captures large copy-pasted blocks using EOF tracking.
|
|
538
|
+
* - `/clear`: Hard-clears active terminal histories and resets session state.
|
|
539
|
+
* - `/models`: Dynamic runtime model hot-swapping (e.g., swapping between Flash and Pro variants).
|
|
540
|
+
* - `/debug`: Toggles active runtime execution telemetry logging.
|
|
541
|
+
* - `/auto-approve`: Grants blanket terminal script permissions to bypass prompt approval blocks.
|
|
542
|
+
* - `/revert`: Pops the most recent change-set log and rolls back mutated files to original states.
|
|
543
|
+
* - `/commit`: Performs git diff staging, executes model summaries to write micro-commits, and executes local git commits.
|
|
544
|
+
*
|
|
545
|
+
* 3. **Workspace Context Gathering & Intention Routing**:
|
|
546
|
+
* - Leverages the Context Agent (`gatherContext`) to perform exploratory scans of the active repository.
|
|
547
|
+
* - Identifies user intention to hot-swap system prompt strategies (`CHAT` for conversational assistance vs. `EXECUTE` for multi-turn modifications).
|
|
548
|
+
* - Compresses gathered repo structures and file contents using high-speed Flash-Lite engines to respect context boundaries and budget costs.
|
|
549
|
+
*
|
|
550
|
+
* 4. **Model Execution & Self-Correction Feedback Loop**:
|
|
551
|
+
* - Passes the target request to Gemini alongside compressed workspace injections.
|
|
552
|
+
* - Recursively processes tool invocations via `processResponse`.
|
|
553
|
+
* - Runs static validation checks (`verifyChangedFiles`) against mutated files to check for compiler/linter bugs.
|
|
554
|
+
* - If files fail validation, enters a self-healing loop by submitting raw diagnostic errors directly back to the AI for remediation.
|
|
555
|
+
*
|
|
556
|
+
* @param workspaceRoot - The relative or absolute path representing the active workspace environment.
|
|
557
|
+
* @param version - The Semantic Version string of the active tool distribution.
|
|
321
558
|
*/
|
|
322
559
|
export async function startAgentLoop(workspaceRoot, version) {
|
|
323
560
|
const chat = createSharedChatSession();
|
|
324
561
|
const inputHandler = new AsyncInputHandler();
|
|
325
562
|
let isRawPasteMode = false;
|
|
326
|
-
//
|
|
563
|
+
// Hook process.stdin.emit to intercept fast stream inputs.
|
|
564
|
+
// When large buffers containing newlines arrive rapidly, we interpret them as a clipboard paste,
|
|
565
|
+
// sanitizing and collapsing them to prevent premature command line submission.
|
|
327
566
|
const originalEmit = process.stdin.emit.bind(process.stdin);
|
|
328
567
|
process.stdin.emit = function (event, ...args) {
|
|
329
568
|
if (!isRawPasteMode && event === 'data' && Buffer.isBuffer(args[0])) {
|
|
@@ -343,9 +582,10 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
343
582
|
let lines = [];
|
|
344
583
|
let isMultiLine = false;
|
|
345
584
|
let canceledGlobal = false;
|
|
585
|
+
// Input collection sub-loop (handles multiline line-by-line gathering)
|
|
346
586
|
while (true) {
|
|
347
587
|
const userInputRaw = await p.text({
|
|
348
|
-
message: isMultiLine ? ' ' : '❯',
|
|
588
|
+
message: isMultiLine ? ' ' : pc.magenta('❯'),
|
|
349
589
|
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
350
590
|
});
|
|
351
591
|
if (p.isCancel(userInputRaw)) {
|
|
@@ -385,6 +625,7 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
385
625
|
if (!userInput) {
|
|
386
626
|
continue;
|
|
387
627
|
}
|
|
628
|
+
// Capture standalone forward slash triggers to open the selection console
|
|
388
629
|
if (userInput === '/') {
|
|
389
630
|
const commandMenu = await p.select({
|
|
390
631
|
message: 'Command Menu',
|
|
@@ -412,7 +653,10 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
412
653
|
if (userInput.length === 0) {
|
|
413
654
|
continue;
|
|
414
655
|
}
|
|
415
|
-
|
|
656
|
+
if (userInput.startsWith('/')) {
|
|
657
|
+
debugLog(`Executing slash command: ${userInput}`);
|
|
658
|
+
}
|
|
659
|
+
// ─── Slash Commands Handling ─────────────────────────────────────
|
|
416
660
|
if (userInput.toLowerCase() === '/paste') {
|
|
417
661
|
p.log.info(pc.cyan('Paste mode activated. Paste your text below, then press Ctrl+D on an empty line to submit. (Ctrl+C to cancel)'));
|
|
418
662
|
try {
|
|
@@ -437,7 +681,7 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
437
681
|
process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
|
|
438
682
|
p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + version)}`);
|
|
439
683
|
p.log.info(`${pc.dim('Workspace:')} ${pc.cyan(workspaceRoot)}`);
|
|
440
|
-
p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu. Type ${pc.yellow('exit')} to leave.`);
|
|
684
|
+
p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
|
|
441
685
|
p.log.success('Chat history cleared.');
|
|
442
686
|
console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
|
|
443
687
|
continue;
|
|
@@ -446,8 +690,10 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
446
690
|
const selectedModel = await p.select({
|
|
447
691
|
message: 'Select AI Model',
|
|
448
692
|
options: [
|
|
693
|
+
{ value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', hint: 'The newest Pro model for complex logic' },
|
|
694
|
+
{ value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', hint: 'Balanced performance' },
|
|
449
695
|
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'Best for complex coding & large context' },
|
|
450
|
-
{
|
|
696
|
+
// {value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'Balanced performance'},
|
|
451
697
|
// {value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'Anthropic: Highly capable, complex reasoning'},
|
|
452
698
|
// {value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', hint: 'Anthropic: Fast and highly intelligent'},
|
|
453
699
|
],
|
|
@@ -459,12 +705,12 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
459
705
|
continue;
|
|
460
706
|
}
|
|
461
707
|
if (userInput.toLowerCase() === '/debug') {
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
p.log.
|
|
708
|
+
const debugMode = toggleDebugMode();
|
|
709
|
+
if (debugMode) {
|
|
710
|
+
p.log.info('Debug mode enabled. Internal logs will now be shown.');
|
|
465
711
|
}
|
|
466
712
|
else {
|
|
467
|
-
p.log.
|
|
713
|
+
p.log.info('Debug mode disabled.');
|
|
468
714
|
}
|
|
469
715
|
continue;
|
|
470
716
|
}
|
|
@@ -540,6 +786,7 @@ ${diffOut}
|
|
|
540
786
|
}
|
|
541
787
|
continue;
|
|
542
788
|
}
|
|
789
|
+
// ─── Main Execution Cycle ────────────────────────────────────────
|
|
543
790
|
const spinner = p.spinner();
|
|
544
791
|
const ac = new AbortController();
|
|
545
792
|
inputHandler.setAbortController(ac);
|
|
@@ -547,6 +794,7 @@ ${diffOut}
|
|
|
547
794
|
inputHandler.start(spinner);
|
|
548
795
|
changeLogger.startChangeSet(userInput);
|
|
549
796
|
let finalInput = userInput;
|
|
797
|
+
// Stage 1: Gather Workspace Context and route intentions
|
|
550
798
|
spinner.start('🔍 Investigating workspace...');
|
|
551
799
|
const chatHistory = chat.getRecentHistory(3);
|
|
552
800
|
const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
|
|
@@ -555,27 +803,28 @@ ${diffOut}
|
|
|
555
803
|
}
|
|
556
804
|
});
|
|
557
805
|
let latestUsage = undefined;
|
|
558
|
-
//
|
|
806
|
+
// Collect any inputs that were queued while the Context Agent was investigating
|
|
559
807
|
const leftoverMsg = inputHandler.getAndClear();
|
|
560
808
|
if (leftoverMsg) {
|
|
561
809
|
gatherRes.chainedMessages.push(leftoverMsg);
|
|
562
810
|
}
|
|
811
|
+
// Resolve the effective target agent mode based on original intent + any chained commands
|
|
563
812
|
let effectiveTargetAgent = gatherRes.targetAgent;
|
|
564
813
|
if (gatherRes.chainedMessages.length > 0) {
|
|
565
814
|
const chainedContent = gatherRes.chainedMessages.join('\n');
|
|
566
815
|
const newIntent = await routeIntent(chainedContent);
|
|
567
816
|
if (gatherRes.contextResult !== null) {
|
|
568
|
-
//
|
|
817
|
+
// If the model previously needed search context, determine if followups change the mode
|
|
569
818
|
effectiveTargetAgent = newIntent.targetAgent;
|
|
570
819
|
}
|
|
571
820
|
else {
|
|
572
|
-
//
|
|
821
|
+
// If not in a context-search flow
|
|
573
822
|
if (gatherRes.targetAgent === 'EXECUTE') {
|
|
574
|
-
//
|
|
823
|
+
// EXECUTE permission sets are immutable during follow-ups to prevent downgrades
|
|
575
824
|
effectiveTargetAgent = 'EXECUTE';
|
|
576
825
|
}
|
|
577
826
|
else {
|
|
578
|
-
//
|
|
827
|
+
// Conversational prompts can escalate to EXECUTE if chained instructions dictate
|
|
579
828
|
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
580
829
|
effectiveTargetAgent = 'EXECUTE';
|
|
581
830
|
}
|
|
@@ -583,30 +832,38 @@ ${diffOut}
|
|
|
583
832
|
}
|
|
584
833
|
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.`;
|
|
585
834
|
}
|
|
586
|
-
// Hot-swap the
|
|
835
|
+
// Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
|
|
587
836
|
debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
|
|
588
837
|
const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
|
|
589
838
|
let dynamicSystemInstruction = config.systemInstruction;
|
|
839
|
+
// Inject gathered workspace directories, dependency configurations, and matching search patterns
|
|
590
840
|
if (gatherRes.contextResult) {
|
|
591
|
-
|
|
592
|
-
debugLog(`
|
|
841
|
+
let contextInjection = buildContextInjection(gatherRes.contextResult);
|
|
842
|
+
debugLog(`Raw context injection size: ${contextInjection.length} chars`);
|
|
843
|
+
// Compress context inputs using Gemini Flash Lite to optimize prompt density and prevent token bloat
|
|
844
|
+
contextInjection = await compressTextUsingFlashLite(contextInjection, 'Summarize the following project context concisely. Preserve all file paths, project types, and the high-level purpose of the read files. Keep it under 2000 characters if possible.');
|
|
845
|
+
debugLog(`Compressed context injection size: ${contextInjection.length} chars`);
|
|
593
846
|
dynamicSystemInstruction += '\n\n' + contextInjection;
|
|
594
847
|
}
|
|
848
|
+
// Apply the dynamic prompt updates and tool registrations to the active chat session
|
|
595
849
|
chat.setAgentConfig(dynamicSystemInstruction, config.tools);
|
|
596
850
|
if (!inputHandler.isCurrentlyPrompting()) {
|
|
597
851
|
spinner.message('Thinking...');
|
|
598
852
|
}
|
|
853
|
+
// Send the formulated prompt payload to the generative model
|
|
599
854
|
let result;
|
|
600
855
|
try {
|
|
601
856
|
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
602
857
|
}
|
|
603
858
|
catch (e) {
|
|
859
|
+
spinner.stop('');
|
|
604
860
|
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
605
861
|
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
606
|
-
spinner.stop('');
|
|
607
|
-
continue;
|
|
608
862
|
}
|
|
609
|
-
|
|
863
|
+
else {
|
|
864
|
+
p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
610
867
|
}
|
|
611
868
|
latestUsage = result.response.usageMetadata?.();
|
|
612
869
|
const grounding = result.response.groundingMetadata?.();
|
|
@@ -620,17 +877,29 @@ ${diffOut}
|
|
|
620
877
|
const calls = result.response.functionCalls();
|
|
621
878
|
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
622
879
|
let agentState = { targetAgent: effectiveTargetAgent };
|
|
880
|
+
let previousChangeCount = changeLogger.getCurrentChangeSet()?.changes.length || 0;
|
|
881
|
+
// Stage 2: Recursive Tool Loops and Automated Self-Correction
|
|
623
882
|
while (correctionAttempts <= MAX_CORRECTIONS) {
|
|
624
883
|
if (finalText === '[Generation stopped by user]')
|
|
625
884
|
break;
|
|
626
885
|
finalText = await processResponse(chat, result, workspaceRoot, inputHandler, agentState, ac.signal);
|
|
627
|
-
debugLog(`processResponse returned finalText (length ${finalText.length}): "${finalText.substring(0,
|
|
886
|
+
debugLog(`processResponse returned finalText (length ${finalText.length}): "${finalText.substring(0, 10)}..."`);
|
|
887
|
+
if (finalText.startsWith('[The AI repeatedly returned empty responses')) {
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
628
890
|
const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
|
|
891
|
+
// If the correction cycle is active but no new changes were registered, the model gave up
|
|
892
|
+
if (correctionAttempts > 0 && currentChanges.length <= previousChangeCount) {
|
|
893
|
+
p.log.warn('AI failed to modify any files during the correction attempt. Aborting auto-correction.');
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
previousChangeCount = currentChanges.length;
|
|
629
897
|
const changedFiles = currentChanges
|
|
630
898
|
.filter((c) => c.action === 'create' || c.action === 'modify')
|
|
631
899
|
.map((c) => c.filePath);
|
|
632
900
|
if (changedFiles.length === 0)
|
|
633
901
|
break;
|
|
902
|
+
// Stage 3: Static code analysis / verification
|
|
634
903
|
p.log.step('Verifying modified files...');
|
|
635
904
|
const verificationErrors = await verifyChangedFiles(workspaceRoot, changedFiles);
|
|
636
905
|
if (!verificationErrors) {
|
|
@@ -643,19 +912,22 @@ ${diffOut}
|
|
|
643
912
|
break;
|
|
644
913
|
}
|
|
645
914
|
p.log.warn(`${pc.yellow('Verification failed. Auto-correcting errors')} (Attempt ${correctionAttempts}/${MAX_CORRECTIONS})...`);
|
|
646
|
-
// Optionally show a snippet of the error to the user so they aren't left in the dark
|
|
647
915
|
const displayError = verificationErrors.split('\n').slice(0, 5).join('\n');
|
|
648
916
|
console.log(pc.dim(` ${displayError.replace(/\n/g, '\n ')}\n ...`));
|
|
649
917
|
debugLog(`Verification failed on attempt ${correctionAttempts}/${MAX_CORRECTIONS}. Errors:\n${verificationErrors}`);
|
|
918
|
+
// Compile compilation and syntax diagnostic warnings into an auto-correction prompt
|
|
650
919
|
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.`;
|
|
651
920
|
spinner.start('Thinking (Correction)...');
|
|
652
921
|
result = await chat.sendMessage(correctionPrompt);
|
|
653
922
|
spinner.stop('');
|
|
654
923
|
}
|
|
924
|
+
// Persist the verified changes into our session change ledger
|
|
655
925
|
changeLogger.commitChangeSet();
|
|
656
926
|
if (finalText) {
|
|
657
927
|
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
|
|
658
|
-
|
|
928
|
+
// Strip out excessive empty lines generated by LLMs to prevent huge visual gaps in marked-terminal
|
|
929
|
+
const cleanText = finalText.replace(/\n([ \t]*\n){2,}/g, '\n\n');
|
|
930
|
+
console.log(marked.parse(cleanText));
|
|
659
931
|
}
|
|
660
932
|
if (latestUsage && latestUsage.remainingBalance !== undefined) {
|
|
661
933
|
p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);
|