minovative-mind-cli 1.0.3 → 1.1.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/dist/commands/chat.js +1 -1
- package/dist/services/agent-tools.d.ts +3 -3
- package/dist/services/agent-tools.js +29 -14
- package/dist/services/agent.d.ts +124 -0
- package/dist/services/agent.js +339 -45
- 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/paste.d.ts +1 -0
- package/dist/utils/paste.js +21 -0
- package/dist/utils/systemPrompts.d.ts +5 -5
- package/dist/utils/systemPrompts.js +72 -33
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
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,16 +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 {
|
|
211
|
+
import { readPaste } from '../utils/paste.js';
|
|
121
212
|
import { marked } from 'marked';
|
|
122
213
|
import { markedTerminal } from 'marked-terminal';
|
|
123
214
|
marked.use(markedTerminal());
|
|
124
215
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Mapping of tool identifiers to user-friendly terminal emojis.
|
|
218
|
+
* Enhances the visual feedback during background tool execution turns.
|
|
219
|
+
*/
|
|
125
220
|
const TOOL_ICONS = {
|
|
126
221
|
read_file: '📖',
|
|
127
222
|
write_file: '✏️',
|
|
@@ -133,6 +228,10 @@ const TOOL_ICONS = {
|
|
|
133
228
|
rename_file: '🚚',
|
|
134
229
|
find_dependencies: '🔗',
|
|
135
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Human-readable translations for tool actions.
|
|
233
|
+
* Used for constructing clear, active-verb descriptive log headers in the CLI.
|
|
234
|
+
*/
|
|
136
235
|
const TOOL_LABELS = {
|
|
137
236
|
read_file: 'Reading file',
|
|
138
237
|
write_file: 'Writing file',
|
|
@@ -145,12 +244,33 @@ const TOOL_LABELS = {
|
|
|
145
244
|
find_dependencies: 'Tracing dependencies',
|
|
146
245
|
};
|
|
147
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
|
+
*/
|
|
148
258
|
function formatToolCall(name, args) {
|
|
149
259
|
const icon = TOOL_ICONS[name] ?? '🔧';
|
|
150
260
|
const label = TOOL_LABELS[name] ?? name;
|
|
151
261
|
switch (name) {
|
|
152
|
-
case 'read_file':
|
|
153
|
-
|
|
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
|
+
}
|
|
154
274
|
case 'write_file':
|
|
155
275
|
return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
|
|
156
276
|
case 'modify_file':
|
|
@@ -172,10 +292,16 @@ function formatToolCall(name, args) {
|
|
|
172
292
|
}
|
|
173
293
|
}
|
|
174
294
|
/**
|
|
175
|
-
* Prompts the user to approve a shell command
|
|
176
|
-
*
|
|
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.
|
|
297
|
+
*
|
|
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.
|
|
177
302
|
*
|
|
178
|
-
*
|
|
303
|
+
* @param command - The terminal string requested for execution.
|
|
304
|
+
* @returns A promise resolving to `true` if approved, or `false` if denied/cancelled.
|
|
179
305
|
*/
|
|
180
306
|
async function requestCommandApproval(command) {
|
|
181
307
|
const mode = getApprovalMode();
|
|
@@ -217,11 +343,49 @@ async function requestCommandApproval(command) {
|
|
|
217
343
|
* Processes a single model response that may contain tool calls.
|
|
218
344
|
* Handles the full tool-call loop: execute → feed results → repeat
|
|
219
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.
|
|
220
380
|
*/
|
|
221
381
|
async function processResponse(chat, result, workspaceRoot, inputHandler, agentState, abortSignal) {
|
|
222
382
|
let response = result.response;
|
|
383
|
+
// Upper limit on autonomous sequential tool executions to prevent out-of-control loops
|
|
223
384
|
let turnCount = 0;
|
|
224
385
|
const MAX_TURNS = 25;
|
|
386
|
+
// Recovery thresholds for handling unexpected empty API payloads
|
|
387
|
+
let emptyRetryCount = 0;
|
|
388
|
+
const MAX_EMPTY_RETRIES = 3;
|
|
225
389
|
// Loop while the model keeps requesting tool calls
|
|
226
390
|
while (true) {
|
|
227
391
|
turnCount++;
|
|
@@ -232,21 +396,50 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
232
396
|
const functionCalls = response.functionCalls();
|
|
233
397
|
if (!functionCalls || functionCalls.length === 0) {
|
|
234
398
|
// No more tool calls — return the final text response
|
|
235
|
-
|
|
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;
|
|
236
425
|
}
|
|
237
|
-
// Process each tool call
|
|
426
|
+
// Process each tool call requested by the model
|
|
238
427
|
const toolResponses = [];
|
|
239
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
|
+
}
|
|
240
433
|
const toolName = fc.name;
|
|
241
434
|
const toolArgs = (fc.args ?? {});
|
|
242
|
-
//
|
|
435
|
+
// Log the ongoing tool action to the console
|
|
243
436
|
p.log.step(formatToolCall(toolName, toolArgs));
|
|
244
|
-
//
|
|
437
|
+
// If executing a CLI/shell command, wait for approval
|
|
245
438
|
if (toolName === 'run_command') {
|
|
246
439
|
await inputHandler.waitForPrompt();
|
|
247
|
-
inputHandler.stop();
|
|
440
|
+
inputHandler.stop(); // Temporarily release raw-mode during blocking interactive prompt
|
|
248
441
|
const approved = await requestCommandApproval(toolArgs.command);
|
|
249
|
-
inputHandler.start();
|
|
442
|
+
inputHandler.start(); // Re-engage raw-mode for continuous background monitoring
|
|
250
443
|
if (!approved) {
|
|
251
444
|
toolResponses.push({
|
|
252
445
|
functionResponse: {
|
|
@@ -260,14 +453,17 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
260
453
|
continue;
|
|
261
454
|
}
|
|
262
455
|
}
|
|
263
|
-
// Execute the tool
|
|
264
|
-
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);
|
|
265
458
|
if (toolResult.error) {
|
|
266
459
|
debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
}
|
|
271
467
|
}
|
|
272
468
|
toolResponses.push({
|
|
273
469
|
functionResponse: {
|
|
@@ -279,12 +475,14 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
279
475
|
},
|
|
280
476
|
});
|
|
281
477
|
}
|
|
478
|
+
// Check if the user entered any feedback or interrupt commands during the tool execution cycle
|
|
282
479
|
await inputHandler.waitForPrompt();
|
|
283
480
|
const queuedMsg = inputHandler.getAndClear();
|
|
284
481
|
let additionalText = undefined;
|
|
285
482
|
if (queuedMsg) {
|
|
286
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.`;
|
|
287
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
|
|
288
486
|
const newIntent = await routeIntent(queuedMsg);
|
|
289
487
|
if (agentState.targetAgent === 'CHAT') {
|
|
290
488
|
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
@@ -295,7 +493,7 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
295
493
|
}
|
|
296
494
|
}
|
|
297
495
|
}
|
|
298
|
-
// Feed tool results back to the model
|
|
496
|
+
// Feed tool results and potential interruption text back to the model
|
|
299
497
|
let followUp;
|
|
300
498
|
try {
|
|
301
499
|
followUp = await chat.sendMessage(toolResponses, additionalText, abortSignal);
|
|
@@ -317,14 +515,57 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
|
|
|
317
515
|
/**
|
|
318
516
|
* Starts the interactive agent chat loop.
|
|
319
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.
|
|
320
558
|
*/
|
|
321
559
|
export async function startAgentLoop(workspaceRoot, version) {
|
|
322
560
|
const chat = createSharedChatSession();
|
|
323
561
|
const inputHandler = new AsyncInputHandler();
|
|
324
|
-
|
|
562
|
+
let isRawPasteMode = false;
|
|
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.
|
|
325
566
|
const originalEmit = process.stdin.emit.bind(process.stdin);
|
|
326
567
|
process.stdin.emit = function (event, ...args) {
|
|
327
|
-
if (event === 'data' && Buffer.isBuffer(args[0])) {
|
|
568
|
+
if (!isRawPasteMode && event === 'data' && Buffer.isBuffer(args[0])) {
|
|
328
569
|
let chunk = args[0].toString();
|
|
329
570
|
// If a single data chunk is longer than 2 characters and contains a newline, it is a paste event.
|
|
330
571
|
// (Normal typing sends 1 character per data event. Enter key sends exactly 1 character).
|
|
@@ -341,9 +582,10 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
341
582
|
let lines = [];
|
|
342
583
|
let isMultiLine = false;
|
|
343
584
|
let canceledGlobal = false;
|
|
585
|
+
// Input collection sub-loop (handles multiline line-by-line gathering)
|
|
344
586
|
while (true) {
|
|
345
587
|
const userInputRaw = await p.text({
|
|
346
|
-
message: isMultiLine ? ' ' : '❯',
|
|
588
|
+
message: isMultiLine ? ' ' : pc.magenta('❯'),
|
|
347
589
|
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
348
590
|
});
|
|
349
591
|
if (p.isCancel(userInputRaw)) {
|
|
@@ -383,11 +625,13 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
383
625
|
if (!userInput) {
|
|
384
626
|
continue;
|
|
385
627
|
}
|
|
628
|
+
// Capture standalone forward slash triggers to open the selection console
|
|
386
629
|
if (userInput === '/') {
|
|
387
630
|
const commandMenu = await p.select({
|
|
388
631
|
message: 'Command Menu',
|
|
389
632
|
options: [
|
|
390
633
|
{ value: '/models', label: '/models', hint: 'Change the active AI model' },
|
|
634
|
+
{ value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
|
|
391
635
|
{ value: '/clear', label: '/clear', hint: 'Clear chat session history' },
|
|
392
636
|
{ value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
|
|
393
637
|
{ value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
|
|
@@ -409,13 +653,35 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
409
653
|
if (userInput.length === 0) {
|
|
410
654
|
continue;
|
|
411
655
|
}
|
|
412
|
-
|
|
413
|
-
|
|
656
|
+
if (userInput.startsWith('/')) {
|
|
657
|
+
debugLog(`Executing slash command: ${userInput}`);
|
|
658
|
+
}
|
|
659
|
+
// ─── Slash Commands Handling ─────────────────────────────────────
|
|
660
|
+
if (userInput.toLowerCase() === '/paste') {
|
|
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)'));
|
|
662
|
+
try {
|
|
663
|
+
isRawPasteMode = true;
|
|
664
|
+
const content = await readPaste();
|
|
665
|
+
isRawPasteMode = false;
|
|
666
|
+
if (!content) {
|
|
667
|
+
p.log.warn('Paste mode closed with no content.');
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
userInput = content;
|
|
671
|
+
p.log.step(pc.cyan(`Loaded ${content.length} characters from paste.`));
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
isRawPasteMode = false;
|
|
675
|
+
p.log.error(`Paste failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (userInput.toLowerCase() === '/clear') {
|
|
414
680
|
chat.clearHistory();
|
|
415
681
|
process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
|
|
416
682
|
p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + version)}`);
|
|
417
683
|
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.`);
|
|
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.`);
|
|
419
685
|
p.log.success('Chat history cleared.');
|
|
420
686
|
console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
|
|
421
687
|
continue;
|
|
@@ -424,6 +690,8 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
424
690
|
const selectedModel = await p.select({
|
|
425
691
|
message: 'Select AI Model',
|
|
426
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: 'The newest Flash model for high speed' },
|
|
427
695
|
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'Best for complex coding & large context' },
|
|
428
696
|
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'Balanced performance' },
|
|
429
697
|
// {value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'Anthropic: Highly capable, complex reasoning'},
|
|
@@ -437,12 +705,12 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
437
705
|
continue;
|
|
438
706
|
}
|
|
439
707
|
if (userInput.toLowerCase() === '/debug') {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
p.log.
|
|
708
|
+
const debugMode = toggleDebugMode();
|
|
709
|
+
if (debugMode) {
|
|
710
|
+
p.log.info('Debug mode enabled. Internal logs will now be shown.');
|
|
443
711
|
}
|
|
444
712
|
else {
|
|
445
|
-
p.log.
|
|
713
|
+
p.log.info('Debug mode disabled.');
|
|
446
714
|
}
|
|
447
715
|
continue;
|
|
448
716
|
}
|
|
@@ -518,6 +786,7 @@ ${diffOut}
|
|
|
518
786
|
}
|
|
519
787
|
continue;
|
|
520
788
|
}
|
|
789
|
+
// ─── Main Execution Cycle ────────────────────────────────────────
|
|
521
790
|
const spinner = p.spinner();
|
|
522
791
|
const ac = new AbortController();
|
|
523
792
|
inputHandler.setAbortController(ac);
|
|
@@ -525,6 +794,7 @@ ${diffOut}
|
|
|
525
794
|
inputHandler.start(spinner);
|
|
526
795
|
changeLogger.startChangeSet(userInput);
|
|
527
796
|
let finalInput = userInput;
|
|
797
|
+
// Stage 1: Gather Workspace Context and route intentions
|
|
528
798
|
spinner.start('🔍 Investigating workspace...');
|
|
529
799
|
const chatHistory = chat.getRecentHistory(3);
|
|
530
800
|
const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
|
|
@@ -533,27 +803,28 @@ ${diffOut}
|
|
|
533
803
|
}
|
|
534
804
|
});
|
|
535
805
|
let latestUsage = undefined;
|
|
536
|
-
//
|
|
806
|
+
// Collect any inputs that were queued while the Context Agent was investigating
|
|
537
807
|
const leftoverMsg = inputHandler.getAndClear();
|
|
538
808
|
if (leftoverMsg) {
|
|
539
809
|
gatherRes.chainedMessages.push(leftoverMsg);
|
|
540
810
|
}
|
|
811
|
+
// Resolve the effective target agent mode based on original intent + any chained commands
|
|
541
812
|
let effectiveTargetAgent = gatherRes.targetAgent;
|
|
542
813
|
if (gatherRes.chainedMessages.length > 0) {
|
|
543
814
|
const chainedContent = gatherRes.chainedMessages.join('\n');
|
|
544
815
|
const newIntent = await routeIntent(chainedContent);
|
|
545
816
|
if (gatherRes.contextResult !== null) {
|
|
546
|
-
//
|
|
817
|
+
// If the model previously needed search context, determine if followups change the mode
|
|
547
818
|
effectiveTargetAgent = newIntent.targetAgent;
|
|
548
819
|
}
|
|
549
820
|
else {
|
|
550
|
-
//
|
|
821
|
+
// If not in a context-search flow
|
|
551
822
|
if (gatherRes.targetAgent === 'EXECUTE') {
|
|
552
|
-
//
|
|
823
|
+
// EXECUTE permission sets are immutable during follow-ups to prevent downgrades
|
|
553
824
|
effectiveTargetAgent = 'EXECUTE';
|
|
554
825
|
}
|
|
555
826
|
else {
|
|
556
|
-
//
|
|
827
|
+
// Conversational prompts can escalate to EXECUTE if chained instructions dictate
|
|
557
828
|
if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
|
|
558
829
|
effectiveTargetAgent = 'EXECUTE';
|
|
559
830
|
}
|
|
@@ -561,30 +832,38 @@ ${diffOut}
|
|
|
561
832
|
}
|
|
562
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.`;
|
|
563
834
|
}
|
|
564
|
-
// Hot-swap the
|
|
835
|
+
// Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
|
|
565
836
|
debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
|
|
566
837
|
const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
|
|
567
838
|
let dynamicSystemInstruction = config.systemInstruction;
|
|
839
|
+
// Inject gathered workspace directories, dependency configurations, and matching search patterns
|
|
568
840
|
if (gatherRes.contextResult) {
|
|
569
|
-
|
|
570
|
-
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`);
|
|
571
846
|
dynamicSystemInstruction += '\n\n' + contextInjection;
|
|
572
847
|
}
|
|
848
|
+
// Apply the dynamic prompt updates and tool registrations to the active chat session
|
|
573
849
|
chat.setAgentConfig(dynamicSystemInstruction, config.tools);
|
|
574
850
|
if (!inputHandler.isCurrentlyPrompting()) {
|
|
575
851
|
spinner.message('Thinking...');
|
|
576
852
|
}
|
|
853
|
+
// Send the formulated prompt payload to the generative model
|
|
577
854
|
let result;
|
|
578
855
|
try {
|
|
579
856
|
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
580
857
|
}
|
|
581
858
|
catch (e) {
|
|
859
|
+
spinner.stop('');
|
|
582
860
|
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
583
861
|
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
584
|
-
spinner.stop('');
|
|
585
|
-
continue;
|
|
586
862
|
}
|
|
587
|
-
|
|
863
|
+
else {
|
|
864
|
+
p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
588
867
|
}
|
|
589
868
|
latestUsage = result.response.usageMetadata?.();
|
|
590
869
|
const grounding = result.response.groundingMetadata?.();
|
|
@@ -598,17 +877,29 @@ ${diffOut}
|
|
|
598
877
|
const calls = result.response.functionCalls();
|
|
599
878
|
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
600
879
|
let agentState = { targetAgent: effectiveTargetAgent };
|
|
880
|
+
let previousChangeCount = changeLogger.getCurrentChangeSet()?.changes.length || 0;
|
|
881
|
+
// Stage 2: Recursive Tool Loops and Automated Self-Correction
|
|
601
882
|
while (correctionAttempts <= MAX_CORRECTIONS) {
|
|
602
883
|
if (finalText === '[Generation stopped by user]')
|
|
603
884
|
break;
|
|
604
885
|
finalText = await processResponse(chat, result, workspaceRoot, inputHandler, agentState, ac.signal);
|
|
605
|
-
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
|
+
}
|
|
606
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;
|
|
607
897
|
const changedFiles = currentChanges
|
|
608
898
|
.filter((c) => c.action === 'create' || c.action === 'modify')
|
|
609
899
|
.map((c) => c.filePath);
|
|
610
900
|
if (changedFiles.length === 0)
|
|
611
901
|
break;
|
|
902
|
+
// Stage 3: Static code analysis / verification
|
|
612
903
|
p.log.step('Verifying modified files...');
|
|
613
904
|
const verificationErrors = await verifyChangedFiles(workspaceRoot, changedFiles);
|
|
614
905
|
if (!verificationErrors) {
|
|
@@ -621,19 +912,22 @@ ${diffOut}
|
|
|
621
912
|
break;
|
|
622
913
|
}
|
|
623
914
|
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
915
|
const displayError = verificationErrors.split('\n').slice(0, 5).join('\n');
|
|
626
916
|
console.log(pc.dim(` ${displayError.replace(/\n/g, '\n ')}\n ...`));
|
|
627
917
|
debugLog(`Verification failed on attempt ${correctionAttempts}/${MAX_CORRECTIONS}. Errors:\n${verificationErrors}`);
|
|
918
|
+
// Compile compilation and syntax diagnostic warnings into an auto-correction prompt
|
|
628
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.`;
|
|
629
920
|
spinner.start('Thinking (Correction)...');
|
|
630
921
|
result = await chat.sendMessage(correctionPrompt);
|
|
631
922
|
spinner.stop('');
|
|
632
923
|
}
|
|
924
|
+
// Persist the verified changes into our session change ledger
|
|
633
925
|
changeLogger.commitChangeSet();
|
|
634
926
|
if (finalText) {
|
|
635
927
|
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
|
|
636
|
-
|
|
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));
|
|
637
931
|
}
|
|
638
932
|
if (latestUsage && latestUsage.remainingBalance !== undefined) {
|
|
639
933
|
p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);
|