minovative-mind-cli 2.8.4 → 2.9.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/README.md CHANGED
@@ -88,7 +88,8 @@ Hot-swap during a session using `/models`:
88
88
 
89
89
  | Model | Best for |
90
90
  | ------------------------- | --------------------------------------------------- |
91
- | **Auto** (default) | Automatically selects (3.6 Flash or 3.5 Flash Lite) |
91
+ | **Auto** (default) | Automatically selects (3.7 Flash or 3.5 Flash Lite) |
92
+ | **Gemini 3.7 Flash** | Next-gen performance, reasoning & fast execution |
92
93
  | **Gemini 3.6 Flash** | Everyday coding — fast and accurate |
93
94
  | **Gemini 3.1 Pro** | Complex architectural changes |
94
95
  | **Gemini 3.5 Flash Lite** | Best for speed and cost efficiency |
@@ -31,6 +31,8 @@ export declare class AsyncInputHandler {
31
31
  private stopped;
32
32
  /** Reference to the AbortController controlling the active AI request to trigger cancellations */
33
33
  private ac;
34
+ /** Timeout reference for reattaching listener after prompting to avoid race conditions */
35
+ private reattachTimeout;
34
36
  /**
35
37
  * Registers the active AbortController for the current AI request.
36
38
  * This is triggered when the user commands a process cancel (e.g., typing "stop").
@@ -45,6 +47,14 @@ export declare class AsyncInputHandler {
45
47
  * @returns True if a text prompt is currently displayed, false otherwise.
46
48
  */
47
49
  isCurrentlyPrompting(): boolean;
50
+ /**
51
+ * Checks if there are any queued user messages pending.
52
+ */
53
+ hasQueuedMessages(): boolean;
54
+ /**
55
+ * Returns a copy of currently queued messages without clearing them.
56
+ */
57
+ peek(): string[];
48
58
  /**
49
59
  * Blocks and waits until any active prompting action is completed.
50
60
  * Guarantees terminal stdout is clean before resuming logs.
@@ -63,16 +73,21 @@ export declare class AsyncInputHandler {
63
73
  private onData;
64
74
  /**
65
75
  * Starts intercepting keystrokes and enables raw terminal processing.
66
- * Saves the original raw mode configuration to ensure a clean restoration later.
76
+ * Clears any stale queued messages and saves the original raw mode configuration.
67
77
  *
68
78
  * @param spinner - The current active Clack spinner UI reference, if any.
69
79
  */
70
80
  start(spinner?: ReturnType<typeof p.spinner>): void;
71
81
  /**
72
- * Disables raw mode, stops intercepting keystrokes, and restores
73
- * the terminal stdin stream to its original raw/cooked state.
82
+ * Disables raw mode, stops intercepting keystrokes, restores
83
+ * the terminal stdin stream to its original raw/cooked state, and
84
+ * completely clears the queued messages.
74
85
  */
75
86
  stop(): void;
87
+ /**
88
+ * Clears all pending queued messages without modifying stream state.
89
+ */
90
+ clear(): void;
76
91
  /**
77
92
  * Clears the spinner reference without stopping the input handler.
78
93
  */
@@ -1,6 +1,12 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { truncateForTerminal } from '../../utils/terminal.js';
4
+ if (process.stdin.setMaxListeners) {
5
+ process.stdin.setMaxListeners(50);
6
+ }
7
+ if (process.stdout.setMaxListeners) {
8
+ process.stdout.setMaxListeners(50);
9
+ }
4
10
  /**
5
11
  * Asynchronous Input Handler (AsyncInputHandler)
6
12
  *
@@ -33,6 +39,8 @@ export class AsyncInputHandler {
33
39
  stopped = true;
34
40
  /** Reference to the AbortController controlling the active AI request to trigger cancellations */
35
41
  ac = null;
42
+ /** Timeout reference for reattaching listener after prompting to avoid race conditions */
43
+ reattachTimeout = null;
36
44
  /**
37
45
  * Registers the active AbortController for the current AI request.
38
46
  * This is triggered when the user commands a process cancel (e.g., typing "stop").
@@ -51,6 +59,18 @@ export class AsyncInputHandler {
51
59
  isCurrentlyPrompting() {
52
60
  return this.isPrompting;
53
61
  }
62
+ /**
63
+ * Checks if there are any queued user messages pending.
64
+ */
65
+ hasQueuedMessages() {
66
+ return this.queue.length > 0;
67
+ }
68
+ /**
69
+ * Returns a copy of currently queued messages without clearing them.
70
+ */
71
+ peek() {
72
+ return [...this.queue];
73
+ }
54
74
  /**
55
75
  * Blocks and waits until any active prompting action is completed.
56
76
  * Guarantees terminal stdout is clean before resuming logs.
@@ -72,7 +92,7 @@ export class AsyncInputHandler {
72
92
  */
73
93
  onData = async (chunk) => {
74
94
  try {
75
- if (this.isPrompting)
95
+ if (this.stopped || this.isPrompting)
76
96
  return;
77
97
  const char = chunk.toString();
78
98
  // Handle Ctrl+C (End of Text ASCII 0x03) immediately
@@ -99,6 +119,9 @@ export class AsyncInputHandler {
99
119
  placeholder: '(Leave blank and press Enter to cancel)',
100
120
  initialValue: char,
101
121
  });
122
+ if (this.stopped) {
123
+ return;
124
+ }
102
125
  let wasAborted = false;
103
126
  if (!p.isCancel(userInput) && userInput.trim()) {
104
127
  const text = userInput.trim();
@@ -114,7 +137,7 @@ export class AsyncInputHandler {
114
137
  p.log.info(pc.cyan(`📥 Queued message: "${text}"`));
115
138
  }
116
139
  }
117
- if (this.spinner && !wasAborted) {
140
+ if (this.spinner && !wasAborted && !this.stopped) {
118
141
  const resumeMsg = this.spinner._lastMessage || 'Resuming execution...';
119
142
  this.spinner.start(resumeMsg);
120
143
  }
@@ -128,9 +151,14 @@ export class AsyncInputHandler {
128
151
  process.stdin.setRawMode(true);
129
152
  }
130
153
  process.stdin.resume();
154
+ if (this.reattachTimeout) {
155
+ clearTimeout(this.reattachTimeout);
156
+ }
131
157
  // Small delay before reattaching listener to avoid capturing duplicate keypress frames
132
- setTimeout(() => {
158
+ this.reattachTimeout = setTimeout(() => {
159
+ this.reattachTimeout = null;
133
160
  if (!this.stopped) {
161
+ process.stdin.removeListener('data', this.onData);
134
162
  process.stdin.on('data', this.onData);
135
163
  }
136
164
  this.isPrompting = false;
@@ -143,56 +171,53 @@ export class AsyncInputHandler {
143
171
  };
144
172
  /**
145
173
  * Starts intercepting keystrokes and enables raw terminal processing.
146
- * Saves the original raw mode configuration to ensure a clean restoration later.
174
+ * Clears any stale queued messages and saves the original raw mode configuration.
147
175
  *
148
176
  * @param spinner - The current active Clack spinner UI reference, if any.
149
177
  */
150
178
  start(spinner) {
179
+ if (this.reattachTimeout) {
180
+ clearTimeout(this.reattachTimeout);
181
+ this.reattachTimeout = null;
182
+ }
151
183
  this.stopped = false;
184
+ this.queue = [];
152
185
  if (spinner) {
153
- this.spinner = spinner;
154
- // Monkey-patch to track the latest message for un-pausing
155
- if (!spinner._isPatched) {
156
- ;
157
- spinner._isPatched = true;
158
- spinner._lastMessage = 'Executing...';
159
- const originalMessage = spinner.message.bind(spinner);
160
- spinner.message = (msg) => {
161
- if (msg) {
162
- ;
163
- spinner._lastMessage = msg;
164
- }
165
- originalMessage(msg ? truncateForTerminal(msg) : msg);
166
- };
167
- const originalStart = spinner.start.bind(spinner);
168
- spinner.start = (msg) => {
169
- if (msg) {
170
- ;
171
- spinner._lastMessage = msg;
172
- }
173
- originalStart(msg ? truncateForTerminal(msg) : msg);
174
- };
175
- }
186
+ this.setSpinner(spinner);
176
187
  }
177
188
  if (process.stdin.isTTY) {
178
189
  this.originalRawMode = process.stdin.isRaw;
179
190
  process.stdin.setRawMode(true);
180
191
  }
181
192
  process.stdin.resume();
193
+ process.stdin.removeListener('data', this.onData);
182
194
  process.stdin.on('data', this.onData);
183
195
  }
184
196
  /**
185
- * Disables raw mode, stops intercepting keystrokes, and restores
186
- * the terminal stdin stream to its original raw/cooked state.
197
+ * Disables raw mode, stops intercepting keystrokes, restores
198
+ * the terminal stdin stream to its original raw/cooked state, and
199
+ * completely clears the queued messages.
187
200
  */
188
201
  stop() {
189
202
  this.stopped = true;
203
+ this.isPrompting = false;
204
+ if (this.reattachTimeout) {
205
+ clearTimeout(this.reattachTimeout);
206
+ this.reattachTimeout = null;
207
+ }
190
208
  process.stdin.removeListener('data', this.onData);
191
209
  if (process.stdin.isTTY) {
192
210
  process.stdin.setRawMode(this.originalRawMode);
193
211
  }
194
212
  process.stdin.resume();
195
213
  this.spinner = null;
214
+ this.queue = [];
215
+ }
216
+ /**
217
+ * Clears all pending queued messages without modifying stream state.
218
+ */
219
+ clear() {
220
+ this.queue = [];
196
221
  }
197
222
  /**
198
223
  * Clears the spinner reference without stopping the input handler.
@@ -13,6 +13,7 @@ import { renderTerminalMarkdown } from '../../utils/terminal.js';
13
13
  import { readPaste } from '../../utils/paste.js';
14
14
  import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled } from '../agent-tools.js';
15
15
  import { ProxyClient, getAndResetTurnUsage } from '../proxyClient.js';
16
+ import { checkByokSubscription } from '../auth.js';
16
17
  import { GEMINI_MODELS, isByokEnabled } from '../../utils/config.js';
17
18
  import { loadCredentials, updateCredentialField } from '../../utils/credentialStore.js';
18
19
  import { getGlobalActiveModel, setGlobalActiveModel, ProxyChatSession } from '../ai.js';
@@ -25,7 +26,7 @@ import { getGlobalActiveModel, setGlobalActiveModel, ProxyChatSession } from '..
25
26
  * - `/paste` : Enter multi-line paste mode using EOF tracking (`Ctrl+D` submission).
26
27
  * - `/plan` : Toggle AI step-by-step implementation planning mode.
27
28
  * - `/clear` : Clear conversation history, wipe terminal screen, and reset CLI header.
28
- * - `/models` : Hot-swap the active generative AI model (Gemini 3.1 Pro, Gemini 3.6 Flash, Gemini 3.5 Flash-Lite, Auto routing).
29
+ * - `/models` : Hot-swap the active generative AI model (Gemini 3.1 Pro, Gemini 3.7 Flash, Gemini 3.6 Flash, Gemini 3.5 Flash-Lite, Auto routing).
29
30
  * - `/debug` : Toggle internal agent telemetry and diagnostic logging.
30
31
  * - `/auto-approve`: Toggle automatic confirmation skipping for terminal shell execution commands.
31
32
  * - `/sub-agents` : Toggle MMAAK Engine for parallel investigation sub-agent orchestration.
@@ -120,10 +121,15 @@ export async function handleSlashCommand(command, context) {
120
121
  // label: 'Claude 5 Sonnet',
121
122
  // hint: 'Best for raw speed and cost efficiency',
122
123
  // },
124
+ {
125
+ value: 'gemini-3.7-flash',
126
+ label: 'Gemini 3.7 Flash',
127
+ hint: 'Next-gen reasoning, speed & balanced performance',
128
+ },
123
129
  {
124
130
  value: 'gemini-3.6-flash',
125
131
  label: 'Gemini 3.6 Flash',
126
- hint: 'Balanced performance & fast',
132
+ hint: 'Everyday coding fast and accurate',
127
133
  },
128
134
  {
129
135
  value: 'gemini-3.5-flash-lite',
@@ -133,9 +139,9 @@ export async function handleSlashCommand(command, context) {
133
139
  {
134
140
  value: 'auto',
135
141
  label: 'Auto (Flash-Lite / Flash)',
136
- hint: 'Dynamically routes between Gemini 3.5 Flash-Lite and Gemini 3.6 based on prompt complexity',
142
+ hint: 'Dynamically routes between Gemini 3.5 Flash-Lite and Gemini 3.7 Flash based on prompt complexity',
137
143
  },
138
- ].filter(o => !(byokEnabled && o.value.includes('claude')));
144
+ ].filter((o) => !(byokEnabled && o.value.includes('claude')));
139
145
  const selectedModel = await p['select']({
140
146
  message: `Select AI Model (Current: ${pc.cyan(currentModel)})`,
141
147
  initialValue: currentModel,
@@ -1046,12 +1052,29 @@ ${diffOut}
1046
1052
  if (!byokEnabled && !creds.geminiApiKey) {
1047
1053
  p.log.error('You must set an API key before enabling BYOK mode.');
1048
1054
  }
1055
+ else if (!byokEnabled) {
1056
+ const subCheck = await checkByokSubscription();
1057
+ if (!subCheck.active) {
1058
+ p.log.error(subCheck.message ||
1059
+ 'A $3.99/month BYOK Subscription is required. Visit https://www.minovativemind.dev/pricing');
1060
+ }
1061
+ else {
1062
+ await updateCredentialField('useByok', true);
1063
+ p.log.success(`BYOK mode is now ${pc.green('Enabled')}`);
1064
+ }
1065
+ }
1049
1066
  else {
1050
- await updateCredentialField('useByok', !byokEnabled);
1051
- p.log.success(`BYOK mode is now ${!byokEnabled ? pc.green('Enabled') : pc.yellow('Disabled')}`);
1067
+ await updateCredentialField('useByok', false);
1068
+ p.log.success(`BYOK mode is now ${pc.yellow('Disabled')}`);
1052
1069
  }
1053
1070
  }
1054
1071
  else if (action === 'set') {
1072
+ const subCheck = await checkByokSubscription();
1073
+ if (!subCheck.active) {
1074
+ p.log.error(subCheck.message ||
1075
+ 'A $3.99/month BYOK Subscription is required to use your own API key. Please visit https://www.minovativemind.dev/pricing to subscribe. The $3.99 is to cover account maintance for you.');
1076
+ return { shouldContinue: true };
1077
+ }
1055
1078
  const key = await p['password']({
1056
1079
  message: 'Enter your Google AI Studio API Key:',
1057
1080
  validate: (v) => (!v ? 'API key is required' : undefined),
@@ -8,12 +8,14 @@
8
8
  * @param error Optional error message from local validation to guide the AI.
9
9
  * @returns The fixed content, or undefined if it was completely valid or couldn't be fixed.
10
10
  */
11
- export declare function validateAndFixSyntax(content: string, filePath: string, error?: string): Promise<string | undefined>;
11
+ export declare function validateAndFixSyntax(content: string, filePath: string, error?: string, abortSignal?: AbortSignal): Promise<string | undefined>;
12
12
  export interface AIFuzzyMatchOptions {
13
13
  /** Optional custom Gemini model override. Defaults to GEMINI_MODELS.FLASH. */
14
14
  model?: string;
15
15
  /** Whether to validate and fix syntax on the updated content. Defaults to true. */
16
16
  validateSyntax?: boolean;
17
+ /** Optional AbortSignal to cancel execution immediately. */
18
+ abortSignal?: AbortSignal;
17
19
  }
18
20
  export interface AIFuzzyMatchResult {
19
21
  /** Indicates whether the fuzzy search matching and edit application succeeded. */
@@ -68,7 +68,9 @@ function cleanOutput(raw) {
68
68
  * @param error Optional error message from local validation to guide the AI.
69
69
  * @returns The fixed content, or undefined if it was completely valid or couldn't be fixed.
70
70
  */
71
- export async function validateAndFixSyntax(content, filePath, error) {
71
+ export async function validateAndFixSyntax(content, filePath, error, abortSignal) {
72
+ if (abortSignal?.aborted)
73
+ return undefined;
72
74
  // 1. First run fast local validation to check if content is already valid or gather error details
73
75
  const localResult = localValidate(filePath, content);
74
76
  if (localResult.isValid && !error) {
@@ -108,7 +110,7 @@ ${win.snippet}
108
110
 
109
111
  Please fix the syntax error in the snippet above and return ONLY the raw repaired code snippet:`;
110
112
  try {
111
- const result = await snippetChat.sendMessage(snippetPrompt);
113
+ const result = await snippetChat.sendMessage(snippetPrompt, undefined, abortSignal);
112
114
  const repairedSnippet = cleanOutput(result.response.text());
113
115
  if (repairedSnippet && repairedSnippet !== win.snippet) {
114
116
  const candidateContent = content.slice(0, win.startPos) + repairedSnippet + content.slice(win.endPos);
@@ -123,6 +125,8 @@ Please fix the syntax error in the snippet above and return ONLY the raw repaire
123
125
  }
124
126
  }
125
127
  }
128
+ if (abortSignal?.aborted)
129
+ return undefined;
126
130
  // 3. Full file repair strategy (for smaller files or when snippet repair was insufficient)
127
131
  const fullSystemInstruction = `
128
132
  <identity>
@@ -150,7 +154,7 @@ ${effectiveError}
150
154
  Content:
151
155
  ${content}`;
152
156
  try {
153
- const result = await fullChat.sendMessage(fullPrompt);
157
+ const result = await fullChat.sendMessage(fullPrompt, undefined, abortSignal);
154
158
  const output = cleanOutput(result.response.text());
155
159
  if (output === 'VALID' || output === '' || output === content) {
156
160
  return undefined;
@@ -185,6 +189,12 @@ export async function aiFuzzyMatch(fileContent, searchContent, replaceContent, f
185
189
  error: 'File content and search content must not be empty.',
186
190
  };
187
191
  }
192
+ if (options.abortSignal?.aborted) {
193
+ return {
194
+ success: false,
195
+ error: 'Operation aborted',
196
+ };
197
+ }
188
198
  const model = options.model || GEMINI_MODELS.FLASH;
189
199
  const shouldValidateSyntax = options.validateSyntax !== false;
190
200
  const systemInstruction = `
@@ -217,7 +227,7 @@ ${replaceContent}
217
227
  File Content:
218
228
  ${fileContent}`;
219
229
  try {
220
- const response = await chat.sendMessage(prompt);
230
+ const response = await chat.sendMessage(prompt, undefined, options.abortSignal);
221
231
  const rawOutput = response.response.text();
222
232
  const output = cleanOutput(rawOutput);
223
233
  if (output === 'UNMATCHED' || !output || output === fileContent) {
@@ -231,7 +241,7 @@ ${fileContent}`;
231
241
  const valResult = localValidate(filePath, finalContent);
232
242
  if (!valResult.isValid) {
233
243
  // Attempt automatic syntax repair on AI output if local validation detected syntax error
234
- const repaired = await validateAndFixSyntax(finalContent, filePath, valResult.error);
244
+ const repaired = await validateAndFixSyntax(finalContent, filePath, valResult.error, options.abortSignal);
235
245
  if (repaired) {
236
246
  finalContent = repaired;
237
247
  }
@@ -264,7 +264,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
264
264
  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.`;
265
265
  p.log.info(pc.cyan(`Sending queued message to AI...`));
266
266
  // Dynamically upgrade agent permissions/intent to EXECUTE mode if the interrupted instruction requires system modifications
267
- const newIntent = await routeIntent(queuedMsg);
267
+ const newIntent = await routeIntent(queuedMsg, '', abortSignal);
268
268
  if (agentState.targetAgent === 'CHAT') {
269
269
  if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
270
270
  agentState.targetAgent = 'EXECUTE';
@@ -125,4 +125,4 @@ export declare const HISTORY_SUMMARIZATION_THRESHOLD = 50;
125
125
  * @param chat - The active ProxyChatSession instance.
126
126
  * @returns A promise resolving to true if history was summarized and updated; false otherwise.
127
127
  */
128
- export declare function summarizeHistoryIfNeeded(chat: any): Promise<boolean>;
128
+ export declare function summarizeHistoryIfNeeded(chat: any, abortSignal?: AbortSignal): Promise<boolean>;