minovative-mind-cli 1.4.1 → 1.4.3

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.
@@ -0,0 +1,203 @@
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
+ import { toggleDebugMode } from '../../utils/logger.js';
8
+ import { changeLogger } from '../changeLogger.js';
9
+ import { printLogo } from '../../utils/logo.js';
10
+ import { readPaste } from '../../utils/paste.js';
11
+ import { setApprovalMode, getApprovalMode } from '../agent-tools.js';
12
+ import { ProxyChatSession } from '../ai.js';
13
+ const execAsync = promisify(exec);
14
+ /**
15
+ * Handles all slash command operations (/paste, /clear, /models, /debug, /auto-approve, /revert, /commit).
16
+ * Returns control state to the caller loop (such as whether to continue/skip, or if a text-override occurred).
17
+ */
18
+ export async function handleSlashCommand(command, context) {
19
+ const { chat, inputHandler, workspaceRoot, version } = context;
20
+ const lowerCommand = command.toLowerCase();
21
+ if (lowerCommand === '/paste') {
22
+ 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)'));
23
+ try {
24
+ const content = await readPaste();
25
+ if (content === null) {
26
+ p.log.warn('Paste mode canceled.');
27
+ return { shouldContinue: true, isRawPasteMode: false };
28
+ }
29
+ if (!content) {
30
+ p.log.warn('Paste mode closed with no content.');
31
+ return { shouldContinue: true, isRawPasteMode: false };
32
+ }
33
+ p.log.step(pc.cyan(`Loaded ${content.length} characters from paste.`));
34
+ return { shouldContinue: false, userInputOverride: content, isRawPasteMode: false };
35
+ }
36
+ catch (err) {
37
+ p.log.error(`Paste failed: ${err instanceof Error ? err.message : String(err)}`);
38
+ return { shouldContinue: true, isRawPasteMode: false };
39
+ }
40
+ }
41
+ if (lowerCommand === '/clear') {
42
+ chat.clearHistory();
43
+ process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
44
+ printLogo();
45
+ p.intro(`${pc.bgCyan(pc.black(' Minovative Mind CLI '))} ${pc.dim('v' + version)}`);
46
+ p.log.info(`${pc.dim('Workspace:')} ${pc.cyan(workspaceRoot)}`);
47
+ 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.`);
48
+ p.log.success('Chat history cleared.');
49
+ console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
50
+ return { shouldContinue: true };
51
+ }
52
+ if (lowerCommand === '/models') {
53
+ const currentModel = chat.getModel();
54
+ const selectedModel = await p['select']({
55
+ message: `Select AI Model (Current: ${pc.cyan(currentModel)})`,
56
+ initialValue: currentModel,
57
+ options: [
58
+ {
59
+ value: 'gemini-3.1-pro-preview',
60
+ label: 'Gemini 3.1 Pro',
61
+ hint: 'The newest Pro model for complex logic',
62
+ },
63
+ {
64
+ value: 'gemini-3.5-flash',
65
+ label: 'Gemini 3.5 Flash',
66
+ hint: 'Balanced performance',
67
+ },
68
+ {
69
+ value: 'gemini-3.1-flash-lite',
70
+ label: 'Gemini 3.1 Flash-Lite',
71
+ hint: 'Ultra-fast and cost-effective',
72
+ },
73
+ ],
74
+ });
75
+ if (!p.isCancel(selectedModel)) {
76
+ chat.setModel(selectedModel);
77
+ p.log.success(`Model successfully switched to ${pc.cyan(selectedModel)}`);
78
+ }
79
+ return { shouldContinue: true };
80
+ }
81
+ if (lowerCommand === '/debug') {
82
+ const debugMode = toggleDebugMode();
83
+ if (debugMode) {
84
+ p.log.info('Debug mode enabled. Internal logs will now be shown.');
85
+ }
86
+ else {
87
+ p.log.info('Debug mode disabled.');
88
+ }
89
+ return { shouldContinue: true };
90
+ }
91
+ if (lowerCommand === '/auto-approve') {
92
+ if (getApprovalMode() === 'skip-all') {
93
+ setApprovalMode('ask');
94
+ p.log.success('Auto-approve disabled. You will be prompted before commands run.');
95
+ }
96
+ else {
97
+ setApprovalMode('skip-all');
98
+ p.log.success('Auto-approve enabled for all future commands in this session.');
99
+ }
100
+ return { shouldContinue: true };
101
+ }
102
+ if (lowerCommand === '/revert') {
103
+ const history = changeLogger.getHistory();
104
+ if (!history || history.length === 0) {
105
+ p.log.warn('No changes to revert.');
106
+ return { shouldContinue: true };
107
+ }
108
+ const lastChangeSet = history[history.length - 1];
109
+ const truncate = (str, max) => (str.length > max ? str.substring(0, max - 3) + '...' : str);
110
+ const revertMenu = await p['select']({
111
+ message: 'Revert Menu',
112
+ options: [
113
+ { value: 'revert_last', label: `Revert last change (${truncate(lastChangeSet.description, 50)})` },
114
+ { value: 'view_history', label: 'View history' },
115
+ { value: 'cancel', label: 'Cancel' },
116
+ ],
117
+ });
118
+ if (p.isCancel(revertMenu) || revertMenu === 'cancel') {
119
+ return { shouldContinue: true };
120
+ }
121
+ let targetTimestamp = lastChangeSet.timestamp;
122
+ if (revertMenu === 'view_history') {
123
+ const historyOptions = history
124
+ .slice()
125
+ .reverse()
126
+ .map((cs, i) => ({
127
+ value: cs.timestamp,
128
+ label: `[${i === 0 ? 'Latest' : `-${i}`}] ${truncate(cs.description, 50)} (${new Date(cs.timestamp).toLocaleTimeString()})`,
129
+ hint: `Reverts this and all ${i} changes after it`,
130
+ }));
131
+ const selectedHistory = await p['select']({
132
+ message: 'Select the point in history to revert back to:',
133
+ options: [...historyOptions, { value: -1, label: 'Cancel' }],
134
+ });
135
+ if (p.isCancel(selectedHistory) || selectedHistory === -1) {
136
+ return { shouldContinue: true };
137
+ }
138
+ targetTimestamp = selectedHistory;
139
+ }
140
+ const spinner = p.spinner();
141
+ spinner.start('Reverting selected changes...');
142
+ try {
143
+ const changesToRevert = changeLogger.popUntil(targetTimestamp);
144
+ const flatChanges = changesToRevert.flatMap((cs) => cs.changes);
145
+ for (const change of flatChanges) {
146
+ const absPath = path.resolve(workspaceRoot, change.filePath);
147
+ if (change.action === 'create') {
148
+ await fs.rm(absPath, { force: true });
149
+ }
150
+ else if (change.originalContent !== null) {
151
+ await fs.writeFile(absPath, change.originalContent, 'utf-8');
152
+ }
153
+ }
154
+ spinner.stop('Reverted successfully.');
155
+ p.log.success(`Reverted ${changesToRevert.length} changeset(s) successfully.`);
156
+ }
157
+ catch (err) {
158
+ spinner.stop('Revert failed.');
159
+ p.log.error(`Failed to revert: ${err instanceof Error ? err.message : String(err)}`);
160
+ }
161
+ return { shouldContinue: true };
162
+ }
163
+ if (lowerCommand === '/commit') {
164
+ const commitSpinner = p.spinner();
165
+ commitSpinner.start('Staging changes and analyzing diff...');
166
+ try {
167
+ await execAsync('git add .', { cwd: workspaceRoot });
168
+ const { stdout: diffOut } = await execAsync('git diff --cached', { cwd: workspaceRoot });
169
+ if (!diffOut || diffOut.trim().length === 0) {
170
+ commitSpinner.stop('No changes to commit.');
171
+ p.log.warn('Git working tree is clean.');
172
+ return { shouldContinue: true };
173
+ }
174
+ commitSpinner.message('Generating commit message...');
175
+ const prompt = `Generate a concise, standard git commit message for the following diff. Only return the commit message text.
176
+
177
+ <diff>
178
+ ${diffOut}
179
+ </diff>`;
180
+ 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.';
181
+ const commitAgent = new ProxyChatSession('gemini-3.1-flash-lite', commitSystemPrompt, [], {});
182
+ const commitResult = await commitAgent.sendMessage(prompt);
183
+ const commitMsg = commitResult.response.text().trim();
184
+ commitSpinner.message('Committing...');
185
+ const tmpMsgPath = path.join(workspaceRoot, '.gemini-commit-msg.tmp');
186
+ await fs.writeFile(tmpMsgPath, commitMsg, 'utf-8');
187
+ try {
188
+ await execAsync(`git commit -F .gemini-commit-msg.tmp`, { cwd: workspaceRoot });
189
+ }
190
+ finally {
191
+ await fs.rm(tmpMsgPath, { force: true });
192
+ }
193
+ commitSpinner.stop('Committed successfully.');
194
+ p.log.success(`Committed with message:\n\n${pc.dim(commitMsg)}`);
195
+ }
196
+ catch (err) {
197
+ commitSpinner.stop('Commit failed.');
198
+ p.log.error(`Failed to commit: ${err instanceof Error ? err.message : String(err)}`);
199
+ }
200
+ return { shouldContinue: true };
201
+ }
202
+ return { shouldContinue: true };
203
+ }
@@ -0,0 +1,39 @@
1
+ import { ProxyChatSession } from '../ai.js';
2
+ import { AsyncInputHandler } from './inputHandler.js';
3
+ import type { AgentState } from './types.js';
4
+ /**
5
+ * Formats a tool execution call into a beautifully stylized console status line.
6
+ * Extracts context-specific arguments to display relevant parameters in real-time.
7
+ *
8
+ * @param name - The identifier of the tool being called (e.g. 'read_file').
9
+ * @param args - The dictionary of parameters supplied to the tool.
10
+ * @returns A fully colorized and formatted ANSI console string.
11
+ */
12
+ export declare function formatToolCall(name: string, args: Record<string, unknown>): string;
13
+ /**
14
+ * Coordinates and executes the recursive model-response tool-execution loop.
15
+ *
16
+ * ### Execution Mechanism:
17
+ * This function processes the initial response from the generative model. If the model determines
18
+ * it needs to execute tools (e.g. read_file, run_command, modify_file) to answer or fulfill the request:
19
+ *
20
+ * 1. **Analyze Tool Requirements**: Parses requested `functionCalls` from the LLM.
21
+ * 2. **Check for User Abortion**: Periodically examines the `AbortSignal` to stop execution if requested.
22
+ * 3. **Manage Safety Approvals**: For high-risk operations (like `run_command`), requests explicit confirmation from the user (or uses auto-approve configurations).
23
+ * 4. **Execute Operations**: Runs requested tool actions via `executeTool` in parallel or series as requested.
24
+ * 5. **Handle Interrupts**: Integrates background user inputs queued in `inputHandler` as new context prompts back into the active LLM context.
25
+ * 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.
26
+ *
27
+ * ### Limits & Recovery Guardrails:
28
+ * - **Turn-Limiting**: Capped at `MAX_TURNS` (100) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
29
+ * - **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.
30
+ *
31
+ * @param chat - The current active proxy chat session.
32
+ * @param result - The current result returned from sending a message to the model.
33
+ * @param workspaceRoot - The absolute/relative path to the workspace root.
34
+ * @param inputHandler - The input handler to check for queued interrupts.
35
+ * @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
36
+ * @param abortSignal - Signal to detect when the operation has been cancelled.
37
+ * @returns A promise that resolves to the final text response.
38
+ */
39
+ export declare function processResponse(chat: ProxyChatSession, result: any, workspaceRoot: string, inputHandler: AsyncInputHandler, agentState: AgentState, abortSignal: AbortSignal): Promise<string>;
@@ -0,0 +1,271 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { debugLog } from '../../utils/logger.js';
4
+ import { executeTool } from '../agent-tools.js';
5
+ import { getPlanExecutionConfig } from '../ai.js';
6
+ import { routeIntent } from '../contextAgent.js';
7
+ import { requestCommandApproval } from './commandApproval.js';
8
+ // ─── Constants ───────────────────────────────────────────────────────
9
+ /**
10
+ * Mapping of tool identifiers to user-friendly terminal emojis.
11
+ * Enhances the visual feedback during background tool execution turns.
12
+ */
13
+ const TOOL_ICONS = {
14
+ read_file: '📖',
15
+ write_file: '✏️',
16
+ modify_file: '🔧',
17
+ list_directory: '📂',
18
+ run_command: '⚡',
19
+ grep_search: '🔍',
20
+ delete_file: '🗑️',
21
+ rename_file: '🚚',
22
+ find_dependencies: '🔗',
23
+ run_analysis_script: '🔬',
24
+ };
25
+ /**
26
+ * Human-readable translations for tool actions.
27
+ * Used for constructing clear, active-verb descriptive log headers in the CLI.
28
+ */
29
+ const TOOL_LABELS = {
30
+ read_file: 'Reading file',
31
+ write_file: 'Writing file',
32
+ modify_file: 'Modifying file',
33
+ list_directory: 'Listing directory',
34
+ run_command: 'Running command',
35
+ grep_search: 'Searching code',
36
+ delete_file: 'Deleting file',
37
+ rename_file: 'Moving file',
38
+ find_dependencies: 'Tracing dependencies',
39
+ run_analysis_script: 'Analyzing code structure',
40
+ };
41
+ // ─── Helpers ─────────────────────────────────────────────────────────
42
+ /**
43
+ * Formats a tool execution call into a beautifully stylized console status line.
44
+ * Extracts context-specific arguments to display relevant parameters in real-time.
45
+ *
46
+ * @param name - The identifier of the tool being called (e.g. 'read_file').
47
+ * @param args - The dictionary of parameters supplied to the tool.
48
+ * @returns A fully colorized and formatted ANSI console string.
49
+ */
50
+ export function formatToolCall(name, args) {
51
+ const icon = TOOL_ICONS[name] ?? '🔧';
52
+ const label = TOOL_LABELS[name] ?? name;
53
+ const formatPath = (path) => pc.cyan(path);
54
+ const argsMap = {
55
+ read_file: () => {
56
+ if (args.startLine !== undefined || args.endLine !== undefined) {
57
+ const start = args.startLine ?? 1;
58
+ const end = args.endLine ?? 'end';
59
+ return pc.dim(` (Lines ${start}-${end})`) + `: ${formatPath(String(args.filePath))}`;
60
+ }
61
+ if (Array.isArray(args.targetElements) && args.targetElements.length > 0) {
62
+ return pc.dim(` (Elements: ${args.targetElements.join(', ')})`) + `: ${formatPath(String(args.filePath))}`;
63
+ }
64
+ return `: ${formatPath(String(args.filePath))}`;
65
+ },
66
+ write_file: () => `: ${formatPath(String(args.filePath))}`,
67
+ modify_file: () => `: ${formatPath(String(args.filePath))}`,
68
+ list_directory: () => `: ${formatPath(String(args.dirPath ?? '.'))}`,
69
+ run_command: () => `: ${pc.yellow(String(args.command))}`,
70
+ grep_search: () => `: ${pc.magenta(String(args.pattern))}`,
71
+ delete_file: () => `: ${pc.red(String(args.filePath))}`,
72
+ rename_file: () => `: ${formatPath(String(args.sourcePath))} -> ${formatPath(String(args.targetPath))}`,
73
+ find_dependencies: () => `: ${formatPath(String(args.filePath))}${args.direction ? ` (${args.direction})` : ''}`,
74
+ run_analysis_script: () => `: ${formatPath(String(args.targetFile ?? 'workspace'))}`,
75
+ };
76
+ const formatter = argsMap[name];
77
+ const details = formatter ? formatter() : '';
78
+ return `${icon} ${label}${details}`;
79
+ }
80
+ /**
81
+ * Coordinates and executes the recursive model-response tool-execution loop.
82
+ *
83
+ * ### Execution Mechanism:
84
+ * This function processes the initial response from the generative model. If the model determines
85
+ * it needs to execute tools (e.g. read_file, run_command, modify_file) to answer or fulfill the request:
86
+ *
87
+ * 1. **Analyze Tool Requirements**: Parses requested `functionCalls` from the LLM.
88
+ * 2. **Check for User Abortion**: Periodically examines the `AbortSignal` to stop execution if requested.
89
+ * 3. **Manage Safety Approvals**: For high-risk operations (like `run_command`), requests explicit confirmation from the user (or uses auto-approve configurations).
90
+ * 4. **Execute Operations**: Runs requested tool actions via `executeTool` in parallel or series as requested.
91
+ * 5. **Handle Interrupts**: Integrates background user inputs queued in `inputHandler` as new context prompts back into the active LLM context.
92
+ * 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.
93
+ *
94
+ * ### Limits & Recovery Guardrails:
95
+ * - **Turn-Limiting**: Capped at `MAX_TURNS` (100) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
96
+ * - **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.
97
+ *
98
+ * @param chat - The current active proxy chat session.
99
+ * @param result - The current result returned from sending a message to the model.
100
+ * @param workspaceRoot - The absolute/relative path to the workspace root.
101
+ * @param inputHandler - The input handler to check for queued interrupts.
102
+ * @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
103
+ * @param abortSignal - Signal to detect when the operation has been cancelled.
104
+ * @returns A promise that resolves to the final text response.
105
+ */
106
+ export async function processResponse(chat, result, workspaceRoot, inputHandler, agentState, abortSignal) {
107
+ let response = result.response;
108
+ // Upper limit on autonomous sequential tool executions to prevent out-of-control loops
109
+ let turnCount = 0;
110
+ const MAX_TURNS = 100;
111
+ // Recovery thresholds for handling unexpected empty API payloads
112
+ let emptyRetryCount = 0;
113
+ const MAX_EMPTY_RETRIES = 3;
114
+ // Loop while the model keeps requesting tool calls
115
+ while (true) {
116
+ turnCount++;
117
+ if (turnCount > MAX_TURNS) {
118
+ p.log.error(`${pc.red('System Error:')} Agent exceeded maximum autonomous turns (${MAX_TURNS}). Force stopping to prevent infinite loop.`);
119
+ try {
120
+ p.log.info(pc.cyan(`Requesting final summary from AI based on gathered information...`));
121
+ const originalTools = chat.tools || [];
122
+ const originalInstruction = chat.systemInstruction || '';
123
+ // Temporarily clear tools to force a pure text response
124
+ chat.setAgentConfig(originalInstruction, []);
125
+ const finalFollowUp = await chat.sendMessage(`[SYSTEM INTERRUPTION] You have exceeded the maximum number of allowed tool operations (${MAX_TURNS} turns). You must now provide a final answer to the user based ONLY on the information you have gathered so far. Do NOT attempt to call any more tools, just answer the user directly.`, undefined, abortSignal);
126
+ // Restore tools
127
+ chat.setAgentConfig(originalInstruction, originalTools);
128
+ const finalOutput = finalFollowUp.response.text();
129
+ if (finalOutput && finalOutput.trim()) {
130
+ return finalOutput;
131
+ }
132
+ return '';
133
+ }
134
+ catch (e) {
135
+ if (e.name === 'AbortError' || e.message?.includes('abort')) {
136
+ p.log.warn(pc.yellow('Generation stopped by user.'));
137
+ return '[Generation stopped by user]';
138
+ }
139
+ return '';
140
+ }
141
+ }
142
+ const functionCalls = response.functionCalls();
143
+ if (!functionCalls || functionCalls.length === 0) {
144
+ // No more tool calls — return the final text response
145
+ let finalOutput = response.text() ?? '';
146
+ if (!finalOutput.trim()) {
147
+ if (emptyRetryCount < MAX_EMPTY_RETRIES) {
148
+ emptyRetryCount++;
149
+ p.log.warn(pc.yellow(`AI returned an empty response. Attempting to recover (retry ${emptyRetryCount}/${MAX_EMPTY_RETRIES})...`));
150
+ // System-driven injection to kickstart the LLM's conversation generation
151
+ 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.';
152
+ try {
153
+ const retry = await chat.sendMessage(wakeUpMessage, undefined, abortSignal);
154
+ response = retry.response;
155
+ continue;
156
+ }
157
+ catch (e) {
158
+ if (e.name === 'AbortError' || e.message?.includes('abort')) {
159
+ p.log.warn(pc.yellow('Generation stopped by user during recovery.'));
160
+ return '[Generation stopped by user]';
161
+ }
162
+ finalOutput = '[System Error: The AI returned an empty response and failed to recover.]';
163
+ }
164
+ }
165
+ else {
166
+ finalOutput =
167
+ '[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.]';
168
+ }
169
+ }
170
+ return finalOutput;
171
+ }
172
+ // Process each tool call requested by the model (using helper to avoid nested loop warning)
173
+ const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal);
174
+ if (execRes.aborted) {
175
+ return '[Generation stopped by user]';
176
+ }
177
+ const toolResponses = execRes.toolResponses;
178
+ // Check if the user entered any feedback or interrupt commands during the tool execution cycle
179
+ await inputHandler.waitForPrompt();
180
+ const queuedMsg = inputHandler.getAndClear();
181
+ let additionalText = undefined;
182
+ if (queuedMsg) {
183
+ 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.`;
184
+ p.log.info(pc.cyan(`Sending queued message to AI...`));
185
+ // Dynamically upgrade agent permissions/intent to EXECUTE mode if the interrupted instruction requires system modifications
186
+ const newIntent = await routeIntent(queuedMsg);
187
+ if (agentState.targetAgent === 'CHAT') {
188
+ if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
189
+ agentState.targetAgent = 'EXECUTE';
190
+ const config = getPlanExecutionConfig();
191
+ chat.setAgentConfig(config.systemInstruction, config.tools);
192
+ p.log.info(pc.yellow(`Upgraded session intent to EXECUTE based on chained message.`));
193
+ }
194
+ }
195
+ }
196
+ // Feed tool results and potential interruption text back to the model
197
+ let followUp;
198
+ try {
199
+ followUp = await chat.sendMessage(toolResponses, additionalText, abortSignal);
200
+ }
201
+ catch (e) {
202
+ if (e.name === 'AbortError' || e.message?.includes('abort')) {
203
+ p.log.warn(pc.yellow('Generation stopped by user.'));
204
+ return '[Generation stopped by user]';
205
+ }
206
+ throw e;
207
+ }
208
+ await inputHandler.waitForPrompt();
209
+ const grounding = followUp.response.groundingMetadata?.();
210
+ if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
211
+ p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
212
+ }
213
+ response = followUp.response;
214
+ }
215
+ }
216
+ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal) {
217
+ const toolResponses = [];
218
+ for (const fc of functionCalls) {
219
+ await inputHandler.waitForPrompt();
220
+ if (abortSignal.aborted) {
221
+ p.log.warn(pc.yellow('Execution aborted by user.'));
222
+ return { toolResponses, aborted: true };
223
+ }
224
+ const toolName = fc.name;
225
+ const toolArgs = (fc.args ?? {});
226
+ // Log the ongoing tool action to the console
227
+ p.log.step(formatToolCall(toolName, toolArgs));
228
+ // If executing a CLI/shell command, wait for approval
229
+ if (toolName === 'run_command') {
230
+ await inputHandler.waitForPrompt();
231
+ inputHandler.stop(); // Temporarily release raw-mode during blocking interactive prompt
232
+ const approved = await requestCommandApproval(toolArgs.command);
233
+ inputHandler.start(); // Re-engage raw-mode for continuous background monitoring
234
+ if (!approved) {
235
+ toolResponses.push({
236
+ functionResponse: {
237
+ name: toolName,
238
+ response: {
239
+ output: '',
240
+ error: 'Command was denied by the user. Do not retry this command. Ask the user how they would like to proceed.',
241
+ },
242
+ },
243
+ });
244
+ continue;
245
+ }
246
+ }
247
+ // Execute the underlying filesystem, shell or search tool logic
248
+ const toolResult = await executeTool(workspaceRoot, toolName, toolArgs, abortSignal);
249
+ await inputHandler.waitForPrompt();
250
+ if (toolResult.error) {
251
+ debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
252
+ // Do not spam the user with expected chunking warnings
253
+ if (!toolResult.error.includes('File is too large')) {
254
+ // Truncate the error message for the terminal UI to prevent console clutter
255
+ // (e.g. hiding the large file previews sent to the AI)
256
+ const displayError = toolResult.error.split('\n')[0].substring(0, 100);
257
+ p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
258
+ }
259
+ }
260
+ toolResponses.push({
261
+ functionResponse: {
262
+ name: toolName,
263
+ response: {
264
+ output: toolResult.output,
265
+ ...(toolResult.error ? { error: toolResult.error } : {}),
266
+ },
267
+ },
268
+ });
269
+ }
270
+ return { toolResponses, aborted: false };
271
+ }
@@ -0,0 +1,17 @@
1
+ import type { ProxyChatSession } from '../ai.js';
2
+ import type { AsyncInputHandler } from './inputHandler.js';
3
+ export interface AgentState {
4
+ targetAgent: 'CHAT' | 'EXECUTE';
5
+ }
6
+ export interface SlashCommandContext {
7
+ chat: ProxyChatSession;
8
+ inputHandler: AsyncInputHandler;
9
+ workspaceRoot: string;
10
+ version: string;
11
+ isRawPasteMode: boolean;
12
+ }
13
+ export interface SlashCommandResult {
14
+ shouldContinue: boolean;
15
+ userInputOverride?: string;
16
+ isRawPasteMode?: boolean;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -15,96 +15,7 @@
15
15
  * 5. **Self-Correction Pipeline**: Compares modified files against TS/Linter diagnostics,
16
16
  * providing feedback on syntax or semantic failures back to the AI for self-healing.
17
17
  */
18
- import * as p from '@clack/prompts';
19
- /**
20
- * Asynchronous Input Handler (AsyncInputHandler)
21
- *
22
- * This class intercepts user keyboard input from standard input (stdin) while
23
- * background processes (e.g., Gemini model generations or long shell tool runs)
24
- * are executing. It provides seamless execution management by letting users:
25
- *
26
- * 1. **Pause execution** at any time by pressing a key.
27
- * 2. **Queue feedback** ("chained messages") without waiting for the entire run to finish.
28
- * 3. **Force abort** the current model or tool run by entering "stop".
29
- *
30
- * ### Raw Mode & Terminal States
31
- * In normal CLI execution, Node.js waits for a line feed (Enter) before emitting input.
32
- * To intercept immediate keystrokes, we put `process.stdin` into *raw mode*.
33
- * While in raw mode, we listen for direct data buffers.
34
- * To avoid visual conflicts with our logging output and Clack's CLI spinners,
35
- * we dynamically pause spinners, detach listeners, disable raw mode, open standard
36
- * interactive text-prompt forms, and resume raw mode and spinners upon completion.
37
- */
38
- export declare class AsyncInputHandler {
39
- /** Queue of pending user feedback/instructions typed during active background execution */
40
- private queue;
41
- /** Guard flag preventing multiple simultaneous input prompt overlays */
42
- private isPrompting;
43
- /** Reference to the Clack CLI spinner which must be paused/restarted during prompts */
44
- private spinner;
45
- /** Stores the terminal's raw mode configuration state before handler activation */
46
- private originalRawMode;
47
- /** Indicates whether the input handler is currently inactive/stopped */
48
- private stopped;
49
- /** Reference to the AbortController controlling the active AI request to trigger cancellations */
50
- private ac;
51
- /**
52
- * Registers the active AbortController for the current AI request.
53
- * This is triggered when the user commands a process cancel (e.g., typing "stop").
54
- *
55
- * @param ac - The AbortController controlling the current generation.
56
- */
57
- setAbortController(ac: AbortController): void;
58
- /**
59
- * Determines if a user-prompt dialog is actively running.
60
- * Useful for coordinating other console logging output to avoid UI overlap.
61
- *
62
- * @returns True if a text prompt is currently displayed, false otherwise.
63
- */
64
- isCurrentlyPrompting(): boolean;
65
- /**
66
- * Blocks and waits until any active prompting action is completed.
67
- * Guarantees terminal stdout is clean before resuming logs.
68
- */
69
- waitForPrompt(): Promise<void>;
70
- /**
71
- * Core stdin event listener. Detects pressed keys, pauses background visual elements,
72
- * handles exit interrupts, and opens a Clack text dialog for input.
73
- *
74
- * Handles recovery of the standard input stream and raw mode states even if
75
- * errors occur during prompt initialization.
76
- *
77
- * @param chunk - The raw terminal buffer containing keypress data.
78
- * @private
79
- */
80
- private onData;
81
- /**
82
- * Starts intercepting keystrokes and enables raw terminal processing.
83
- * Saves the original raw mode configuration to ensure a clean restoration later.
84
- *
85
- * @param spinner - The current active Clack spinner UI reference, if any.
86
- */
87
- start(spinner?: ReturnType<typeof p.spinner>): void;
88
- /**
89
- * Disables raw mode, stops intercepting keystrokes, and restores
90
- * the terminal stdin stream to its original raw/cooked state.
91
- */
92
- stop(): void;
93
- /**
94
- * Retrieves and flushes all user feedback messages accumulated during execution.
95
- *
96
- * @returns A concatenated string of all queued user messages separated by newlines,
97
- * or an empty string if nothing was queued.
98
- */
99
- getAndClear(): string;
100
- }
101
- /**
102
- * Starts the interactive agent chat loop.
103
- * Runs until the user types "exit", "quit", or presses Ctrl+C.
104
- *
105
- * @param workspaceRoot - The root directory of the workspace.
106
- * @param version - The active version of the CLI utility.
107
- */
18
+ export { AsyncInputHandler } from './agent/inputHandler.js';
108
19
  /**
109
20
  * Starts and orchestrates the primary interactive command-line interface (REPL) loop.
110
21
  *
@@ -120,13 +31,7 @@ export declare class AsyncInputHandler {
120
31
  * - Intercepts and filters OS clipboard paste buffers to allow inserting massive scripts cleanly.
121
32
  *
122
33
  * 2. **Slash Commands Processing**:
123
- * - `/paste`: Interactively captures large copy-pasted blocks using EOF tracking.
124
- * - `/clear`: Hard-clears active terminal histories and resets session state.
125
- * - `/models`: Dynamic runtime model hot-swapping (e.g., swapping between Flash and Pro variants).
126
- * - `/debug`: Toggles active runtime execution telemetry logging.
127
- * - `/auto-approve`: Grants blanket terminal script permissions to bypass prompt approval blocks.
128
- * - `/revert`: Pops the most recent change-set log and rolls back mutated files to original states.
129
- * - `/commit`: Performs git diff staging, executes model summaries to write micro-commits, and executes local git commits.
34
+ * - Delegated to `handleSlashCommand` within `src/services/agent/slashCommands.ts`.
130
35
  *
131
36
  * 3. **Workspace Context Gathering & Intention Routing**:
132
37
  * - Leverages the Context Agent (`gatherContext`) to perform exploratory scans of the active repository.