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.
@@ -315,15 +315,19 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
315
315
  inputHandler.start(spinner);
316
316
  changeLogger.startChangeSet(userInput);
317
317
  let finalInput = userInput;
318
+ if (ac.signal.aborted)
319
+ return;
318
320
  // Inter-turn cooling-off delay to allow API token buckets to settle before running intent routing
319
321
  await new Promise((resolve) => setTimeout(resolve, TPM_COOLING_DELAYS.INTER_TURN_MS));
322
+ if (ac.signal.aborted)
323
+ return;
320
324
  // Stage 0: Summarize chat history if length threshold is met
321
325
  if (!cachedContextResult) {
322
326
  const isDebug = isDebugOn();
323
327
  if (!inputHandler.isCurrentlyPrompting() && !isDebug) {
324
328
  spinner.start('Checking conversation history...');
325
329
  }
326
- const historySummarized = await summarizeHistoryIfNeeded(chat);
330
+ const historySummarized = await summarizeHistoryIfNeeded(chat, ac.signal);
327
331
  if (historySummarized) {
328
332
  debugLog('Chat history was summarized and compressed prior to context investigation.');
329
333
  }
@@ -331,6 +335,8 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
331
335
  spinner.stop();
332
336
  }
333
337
  }
338
+ if (ac.signal.aborted)
339
+ return;
334
340
  // Stage 1: Gather Workspace Context and route intentions
335
341
  spinner.start('🔍 Investigating workspace...');
336
342
  let gatherRes = { targetAgent: 'EXECUTE', chainedMessages: [], contextResult: cachedContextResult || null };
@@ -340,19 +346,34 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
340
346
  collector.startTimer('contextGather');
341
347
  const chatHistory = chat.getRecentHistory(3);
342
348
  const toolLogs = [];
343
- gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
344
- if (!inputHandler.isCurrentlyPrompting()) {
345
- spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
346
- }
347
- }, (toolMsg, label) => {
348
- const formattedLog = `${pc.dim(`[${label}]`)} ${toolMsg}`;
349
- toolLogs.push(formattedLog);
350
- if (!inputHandler.isCurrentlyPrompting()) {
351
- spinner.message(`🔍 Investigating workspace... ${formattedLog}`);
349
+ try {
350
+ gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
351
+ if (!inputHandler.isCurrentlyPrompting()) {
352
+ spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
353
+ }
354
+ }, (toolMsg, label) => {
355
+ const formattedLog = `${pc.dim(`[${label}]`)} ${toolMsg}`;
356
+ toolLogs.push(formattedLog);
357
+ if (!inputHandler.isCurrentlyPrompting()) {
358
+ spinner.message(`🔍 Investigating workspace... ${formattedLog}`);
359
+ }
360
+ });
361
+ }
362
+ catch (gatherErr) {
363
+ spinner.stop();
364
+ process.stdout.write('\x1b[2K\r');
365
+ if (ac.signal.aborted || gatherErr?.name === 'AbortError' || gatherErr?.message?.includes('abort')) {
366
+ return;
352
367
  }
353
- });
368
+ throw gatherErr;
369
+ }
354
370
  if (collector)
355
371
  collector.stopTimer('contextGather');
372
+ if (ac.signal.aborted) {
373
+ spinner.stop();
374
+ process.stdout.write('\x1b[2K\r');
375
+ return;
376
+ }
356
377
  // Print all collected logs at once to prevent flickering, while spinner is stopped
357
378
  if (toolLogs.length > 0) {
358
379
  spinner.stop();
@@ -361,6 +382,11 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
361
382
  }
362
383
  // Post-investigation cooling-off delay to allow API Tokens-Per-Minute (TPM) sliding window to settle
363
384
  await new Promise((resolve) => setTimeout(resolve, TPM_COOLING_DELAYS.POST_INVESTIGATION_MS));
385
+ if (ac.signal.aborted) {
386
+ spinner.stop();
387
+ process.stdout.write('\x1b[2K\r');
388
+ return;
389
+ }
364
390
  }
365
391
  let latestUsage = undefined;
366
392
  // Collect any inputs that were queued while the Context Agent was investigating
@@ -372,7 +398,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
372
398
  let effectiveTargetAgent = gatherRes.targetAgent;
373
399
  if (gatherRes.chainedMessages.length > 0) {
374
400
  const chainedContent = gatherRes.chainedMessages.join('\n');
375
- const newIntent = await routeIntent(chainedContent);
401
+ const newIntent = await routeIntent(chainedContent, '', ac.signal);
376
402
  if (gatherRes.contextResult !== null) {
377
403
  // If the model previously needed search context, determine if followups change the mode
378
404
  effectiveTargetAgent = newIntent.targetAgent;
@@ -392,6 +418,11 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
392
418
  }
393
419
  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.`;
394
420
  }
421
+ if (ac.signal.aborted) {
422
+ spinner.stop();
423
+ process.stdout.write('\x1b[2K\r');
424
+ return;
425
+ }
395
426
  // Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
396
427
  debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
397
428
  let config;
@@ -430,7 +461,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
430
461
  else {
431
462
  spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
432
463
  spinner.start(pc.blue('🧠 Evaluating execution complexity...'));
433
- const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
464
+ const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0, '', ac.signal);
434
465
  spinner.stop();
435
466
  process.stdout.write('\x1b[2K\r');
436
467
  selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE : GEMINI_MODELS.FLASH;
@@ -441,6 +472,11 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
441
472
  else {
442
473
  chat.setModel(getGlobalActiveModel());
443
474
  }
475
+ if (ac.signal.aborted) {
476
+ spinner.stop();
477
+ process.stdout.write('\x1b[2K\r');
478
+ return;
479
+ }
444
480
  if (gatherRes.contextResult) {
445
481
  if (!inputHandler.isCurrentlyPrompting()) {
446
482
  spinner.stop(pc.green('🔍 Investigation complete.'));
@@ -468,6 +504,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
468
504
  const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
469
505
  const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
470
506
  if (typeof handledByOrchestrator === 'string') {
507
+ inputHandler.stop();
471
508
  const isStopped = handledByOrchestrator.includes('Generation stopped') ||
472
509
  handledByOrchestrator.includes('[Generation stopped');
473
510
  if (isStopped) {
@@ -618,6 +655,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
618
655
  // Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
619
656
  const correctionRes = await executeSelfCorrectionLoop(chat, result, workspaceRoot, inputHandler, effectiveTargetAgent, ac.signal, spinner, userInput, isPlanMode);
620
657
  const finalText = correctionRes.finalText;
658
+ inputHandler.stop();
621
659
  // Update latest usage metadata to reflect all completed turns
622
660
  const usage = getAndResetTurnUsage();
623
661
  const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
@@ -710,7 +748,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
710
748
  if (!chatSessionState.title) {
711
749
  chatSessionState.title = 'Generating title...';
712
750
  try {
713
- const title = await generateChatTitle(userInput);
751
+ const title = await generateChatTitle(userInput, ac.signal);
714
752
  chatSessionState.title = title;
715
753
  await chatHistoryService.saveSession({
716
754
  id: chatSessionState.id,
@@ -799,10 +837,15 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
799
837
  // label: 'Claude 5 Sonnet',
800
838
  // hint: 'Best for raw speed and cost efficiency',
801
839
  // },
840
+ {
841
+ value: 'gemini-3.7-flash',
842
+ label: 'Gemini 3.7 Flash',
843
+ hint: 'Next-gen reasoning, speed & balanced performance',
844
+ },
802
845
  {
803
846
  value: 'gemini-3.6-flash',
804
847
  label: 'Gemini 3.6 Flash',
805
- hint: 'Balanced performance & fast',
848
+ hint: 'Everyday coding fast and accurate',
806
849
  },
807
850
  {
808
851
  value: 'gemini-3.5-flash-lite',
@@ -812,7 +855,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
812
855
  {
813
856
  value: 'auto',
814
857
  label: 'Auto (Flash-Lite / Flash)',
815
- hint: 'Dynamically routes between Gemini 3.5 Flash-Lite and Gemini 3.6 based on prompt complexity',
858
+ hint: 'Dynamically routes between Gemini 3.5 Flash-Lite and Gemini 3.7 Flash based on prompt complexity',
816
859
  },
817
860
  ].filter((o) => !(byokEnabled && o.value.includes('claude')));
818
861
  const selectedModel = await p['select']({
@@ -842,10 +885,14 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
842
885
  return { planModeReturn, contextResult: gatherRes.contextResult };
843
886
  }
844
887
  catch (err) {
888
+ if (ac.signal.aborted || err?.name === 'AbortError' || err?.message?.includes('abort')) {
889
+ return;
890
+ }
845
891
  const message = err instanceof Error ? err.message : String(err);
846
892
  p.log.error(`${pc.red('Error:')} ${message}`);
847
893
  }
848
894
  finally {
895
+ inputHandler.stop();
849
896
  spinner.stop();
850
897
  process.stdout.write('\x1b[2K\r');
851
898
  // Persist any verified or partial changes into our session change ledger.
@@ -955,7 +1002,7 @@ export const HISTORY_SUMMARIZATION_THRESHOLD = 50;
955
1002
  * @param chat - The active ProxyChatSession instance.
956
1003
  * @returns A promise resolving to true if history was summarized and updated; false otherwise.
957
1004
  */
958
- export async function summarizeHistoryIfNeeded(chat) {
1005
+ export async function summarizeHistoryIfNeeded(chat, abortSignal) {
959
1006
  const history = chat.getRawHistory();
960
1007
  if (!history || history.length < HISTORY_SUMMARIZATION_THRESHOLD) {
961
1008
  return false;
@@ -966,7 +1013,7 @@ export async function summarizeHistoryIfNeeded(chat) {
966
1013
  const olderHistory = history.slice(0, history.length - recentCount);
967
1014
  const recentHistory = history.slice(history.length - recentCount);
968
1015
  debugLog(`Summarizing ${olderHistory.length} older chat history entries...`);
969
- const summaryText = await summarizeChatHistory(olderHistory);
1016
+ const summaryText = await summarizeChatHistory(olderHistory, abortSignal);
970
1017
  if (summaryText && summaryText.trim().length > 0) {
971
1018
  const summaryContent = [
972
1019
  {
@@ -984,6 +1031,9 @@ export async function summarizeHistoryIfNeeded(chat) {
984
1031
  }
985
1032
  }
986
1033
  catch (error) {
1034
+ if (abortSignal?.aborted || error?.name === 'AbortError' || error?.message?.includes('abort')) {
1035
+ return false;
1036
+ }
987
1037
  debugLog(`Failed to summarize chat history: ${error?.message || error}`);
988
1038
  }
989
1039
  return false;
@@ -998,15 +1048,15 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
998
1048
  let intentVerified = false;
999
1049
  let isFixingCodeError = false;
1000
1050
  while (correctionAttempts <= MAX_CORRECTIONS) {
1001
- if (finalText === '[Generation stopped by user]')
1051
+ if (signal.aborted || finalText === '[Generation stopped by user]')
1002
1052
  break;
1003
1053
  const historyLengthBefore = chat.getRawHistory().length;
1004
1054
  const currentText = await processResponse(chat, result, workspaceRoot, inputHandler, agentState, signal);
1005
1055
  const historyLengthAfter = chat.getRawHistory().length;
1006
1056
  const usedTools = historyLengthAfter > historyLengthBefore;
1007
1057
  debugLog(`processResponse returned text (length ${currentText.length}): "${currentText.substring(0, 10)}..."`);
1008
- if (currentText === '[Generation stopped by user]') {
1009
- finalText = currentText;
1058
+ if (currentText === '[Generation stopped by user]' || signal.aborted) {
1059
+ finalText = '[Generation stopped by user]';
1010
1060
  break;
1011
1061
  }
1012
1062
  if (currentText.startsWith('[The AI repeatedly returned empty responses')) {
@@ -1032,7 +1082,18 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
1032
1082
  debugLog('AI failed to execute tools or modify files during the correction attempt. Retrying...');
1033
1083
  const forcePrompt = `AUTOMATED SYSTEM CHECK: You did not execute any tools or modify files. You MUST use your tools (such as run_command or modify_file) to investigate and apply a fix for the previously mentioned errors. Do not just explain the issue.`;
1034
1084
  spinner.start('Thinking (Correction)...');
1035
- result = await chat.sendMessage(forcePrompt);
1085
+ try {
1086
+ result = await chat.sendMessage(forcePrompt, undefined, signal);
1087
+ }
1088
+ catch (e) {
1089
+ spinner.stop();
1090
+ process.stdout.write('\x1b[2K\r');
1091
+ if (e.name === 'AbortError' || e.message?.includes('abort') || signal.aborted) {
1092
+ p.log.warn(pc.yellow('Generation stopped by user during correction.'));
1093
+ break;
1094
+ }
1095
+ throw e;
1096
+ }
1036
1097
  spinner.stop();
1037
1098
  process.stdout.write('\x1b[2K\r');
1038
1099
  correctionAttempts++;
@@ -1057,7 +1118,7 @@ If you are not done, please continue working using your other tools.`;
1057
1118
  catch (e) {
1058
1119
  spinner.stop();
1059
1120
  process.stdout.write('\x1b[2K\r');
1060
- if (e.name === 'AbortError' || e.message?.includes('abort')) {
1121
+ if (e.name === 'AbortError' || e.message?.includes('abort') || signal.aborted) {
1061
1122
  p.log.warn(pc.yellow('Generation stopped by user during verification.'));
1062
1123
  break;
1063
1124
  }
@@ -1130,7 +1191,18 @@ If you are not done, please continue working using your other tools.`;
1130
1191
  // Compile compilation and syntax diagnostic warnings into an auto-correction prompt
1131
1192
  const correctionPrompt = `AUTOMATED SYSTEM CHECK: Your previous changes resulted in the following issues:\n\n${combinedIssuesForAI}\n\nPlease analyze these issues and use your file modification tools to fix them.`;
1132
1193
  spinner.start('Thinking (Correction)...');
1133
- result = await chat.sendMessage(correctionPrompt);
1194
+ try {
1195
+ result = await chat.sendMessage(correctionPrompt, undefined, signal);
1196
+ }
1197
+ catch (e) {
1198
+ spinner.stop();
1199
+ process.stdout.write('\x1b[2K\r');
1200
+ if (e.name === 'AbortError' || e.message?.includes('abort') || signal.aborted) {
1201
+ p.log.warn(pc.yellow('Generation stopped by user during correction.'));
1202
+ break;
1203
+ }
1204
+ throw e;
1205
+ }
1134
1206
  spinner.stop();
1135
1207
  process.stdout.write('\x1b[2K\r');
1136
1208
  }
@@ -80,7 +80,7 @@ export declare function getPlanModeConfig(): {
80
80
  * Compresses a large string of text using gemini-3.5-flash-lite.
81
81
  * Used for shrinking context payloads to prevent OOM/choking.
82
82
  */
83
- export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any, force?: boolean): Promise<string>;
83
+ export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any, force?: boolean, abortSignal?: AbortSignal): Promise<string>;
84
84
  /**
85
85
  * Returns the tool declarations for the read-only Context Agent.
86
86
  * Extracted as a reusable function so investigation sub-agents can import
@@ -96,8 +96,8 @@ export declare function createHistorySummarizerSession(): any;
96
96
  /**
97
97
  * Summarizes an array of Content history entries using Gemini Flash Lite.
98
98
  */
99
- export declare function summarizeChatHistory(history: Content[]): Promise<string>;
99
+ export declare function summarizeChatHistory(history: Content[], abortSignal?: AbortSignal): Promise<string>;
100
100
  /**
101
101
  * Generates a concise title for a chat session based on the user's first message.
102
102
  */
103
- export declare function generateChatTitle(firstMessage: string): Promise<string>;
103
+ export declare function generateChatTitle(firstMessage: string, abortSignal?: AbortSignal): Promise<string>;
@@ -1,8 +1,8 @@
1
- import { SchemaType, } from '@google/generative-ai';
1
+ import { SchemaType } from '@google/generative-ai';
2
2
  import { GEMINI_MODELS, DEFAULT_MODEL, MAX_OUTPUT_TOKENS, isByokEnabled } from '../utils/config.js';
3
3
  import { getToolDeclarations } from './agent-tools.js';
4
4
  import { getMetricCollector } from './metrics.js';
5
- import { getAuthorizedIdToken } from './auth.js';
5
+ import { getAuthorizedIdToken, checkByokSubscription } from './auth.js';
6
6
  import { debugLog } from '../utils/logger.js';
7
7
  import { readCache, writeCache } from '../utils/projectStorage.js';
8
8
  import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
@@ -186,6 +186,14 @@ export class ProxyChatSession {
186
186
  if (!idToken) {
187
187
  throw new Error('You are not signed in. Please run `minovative-mind-cli login` first.');
188
188
  }
189
+ const byokEnabled = await isByokEnabled();
190
+ if (byokEnabled) {
191
+ const subCheck = await checkByokSubscription();
192
+ if (!subCheck.active) {
193
+ throw new Error(subCheck.message ||
194
+ '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.');
195
+ }
196
+ }
189
197
  // Convert message to Part, truncating text to prevent memory blowout
190
198
  let newParts;
191
199
  if (typeof message === 'string') {
@@ -228,7 +236,6 @@ export class ProxyChatSession {
228
236
  // Prune old history before sending to keep payload bounded
229
237
  await this.pruneHistory();
230
238
  const effectiveGenerationConfig = { ...this.generationConfig };
231
- const byokEnabled = await isByokEnabled();
232
239
  let result;
233
240
  if (byokEnabled) {
234
241
  const creds = await loadCredentials();
@@ -341,9 +348,11 @@ export function getPlanModeConfig() {
341
348
  * Compresses a large string of text using gemini-3.5-flash-lite.
342
349
  * Used for shrinking context payloads to prevent OOM/choking.
343
350
  */
344
- export async function compressTextUsingFlashLite(text, instruction = "<directives>\nSummarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Make sure it's understandable without the fluff.\n</directives>", inlineData, force = false) {
351
+ export async function compressTextUsingFlashLite(text, instruction = "<directives>\nSummarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Make sure it's understandable without the fluff.\n</directives>", inlineData, force = false, abortSignal) {
345
352
  if (!text || (!force && text.length < 1000 && !inlineData))
346
353
  return text; // Don't compress tiny texts unless forced
354
+ if (abortSignal?.aborted)
355
+ return text;
347
356
  try {
348
357
  const idToken = await getAuthorizedIdToken();
349
358
  if (!idToken)
@@ -357,13 +366,19 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
357
366
  const byokEnabled = await isByokEnabled();
358
367
  let result;
359
368
  if (byokEnabled) {
369
+ const subCheck = await checkByokSubscription();
370
+ if (!subCheck.active) {
371
+ throw new Error(subCheck.message ||
372
+ 'A $3.99/month BYOK Subscription is required. Visit https://www.minovativemind.dev/pricing');
373
+ }
360
374
  const creds = await loadCredentials();
361
375
  result = await proxyClient.generateViaBYOK(creds.geminiApiKey, model, contents, [], // no tools
362
- undefined, instruction, { temperature: 0.2 });
376
+ undefined, instruction, { temperature: 0.2 }, undefined, abortSignal);
363
377
  }
364
378
  else {
365
379
  result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
366
- undefined, instruction, { temperature: 0.2 });
380
+ undefined, instruction, { temperature: 0.2 }, // low temp for factual summary
381
+ undefined, abortSignal);
367
382
  }
368
383
  let textPart = '';
369
384
  if (result.parts) {
@@ -379,6 +394,9 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
379
394
  return summary ? summary : text;
380
395
  }
381
396
  catch (error) {
397
+ if (abortSignal?.aborted || error?.name === 'AbortError' || error?.message?.includes('abort')) {
398
+ return text;
399
+ }
382
400
  debugLog(`Failed to compress text using flash-lite: ${error}`);
383
401
  if (error?.status === 401 ||
384
402
  error?.status === 403 ||
@@ -483,7 +501,7 @@ export function getContextToolDeclarations() {
483
501
  },
484
502
  {
485
503
  name: 'run_analysis_script',
486
- description: "Write and execute a disposable analysis script to structurally map code in the workspace. Use this to get exact line ranges for functions, classes, and variables by leveraging the language's native AST parser (e.g., TypeScript compiler API, Python ast module, go/parser). You can also use this to probe the development environment — detecting available runtimes (e.g., node --version, python3 --version), checking if ports are in use, identifying project type (monorepo, package manager), or diagnosing system-level issues (disk space, memory) that may affect execution. For complex investigation tasks, you can write lightweight ML scripts (e.g., TF-IDF cosine similarity to rank file relevance, Z-score outlier detection for anomalous log lines, K-Means clustering, or Naive Bayes classification). Default to \"node\" for generic math/analysis as a safe baseline, but act like a native inhabitant of the host environment — if Python, Go, Rust, or specialized libraries are available in the project context, leverage the host's native runtimes and standard libraries for maximum efficiency. The script is executed from a temporary directory and automatically cleaned up after execution. Output should be structured JSON to stdout. Use the results to make precise read_file calls with exact startLine/endLine instead of guessing. CRITICAL: Do not use this tool on binary, document, or non-code files (e.g. PDF, image, audio, docx).",
504
+ description: 'Write and execute a disposable analysis script to structurally map code in the workspace. Use this to get exact line ranges for functions, classes, and variables by leveraging the language\'s native AST parser (e.g., TypeScript compiler API, Python ast module, go/parser). You can also use this to probe the development environment — detecting available runtimes (e.g., node --version, python3 --version), checking if ports are in use, identifying project type (monorepo, package manager), or diagnosing system-level issues (disk space, memory) that may affect execution. For complex investigation tasks, you can write lightweight ML scripts (e.g., TF-IDF cosine similarity to rank file relevance, Z-score outlier detection for anomalous log lines, K-Means clustering, or Naive Bayes classification). Default to "node" for generic math/analysis as a safe baseline, but act like a native inhabitant of the host environment — if Python, Go, Rust, or specialized libraries are available in the project context, leverage the host\'s native runtimes and standard libraries for maximum efficiency. The script is executed from a temporary directory and automatically cleaned up after execution. Output should be structured JSON to stdout. Use the results to make precise read_file calls with exact startLine/endLine instead of guessing. CRITICAL: Do not use this tool on binary, document, or non-code files (e.g. PDF, image, audio, docx).',
487
505
  parameters: {
488
506
  type: SchemaType.OBJECT,
489
507
  properties: {
@@ -587,22 +605,35 @@ export function createHistorySummarizerSession() {
587
605
  let model = getGlobalActiveModel();
588
606
  if (model === 'auto' || model.includes('claude'))
589
607
  model = GEMINI_MODELS.FLASH_LITE;
590
- return new ProxyChatSession(model, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, [], { temperature: 0.2, maxOutputTokens: MAX_OUTPUT_TOKENS });
608
+ return new ProxyChatSession(model, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, [], {
609
+ temperature: 0.2,
610
+ maxOutputTokens: MAX_OUTPUT_TOKENS,
611
+ });
591
612
  }
592
613
  /**
593
614
  * Summarizes an array of Content history entries using Gemini Flash Lite.
594
615
  */
595
- export async function summarizeChatHistory(history) {
616
+ export async function summarizeChatHistory(history, abortSignal) {
596
617
  if (!history || history.length === 0) {
597
618
  return '';
598
619
  }
620
+ if (abortSignal?.aborted) {
621
+ const err = new Error('Operation aborted');
622
+ err.name = 'AbortError';
623
+ throw err;
624
+ }
599
625
  try {
600
626
  const session = createHistorySummarizerSession();
601
627
  session.loadRawHistory(JSON.parse(JSON.stringify(history)));
602
- const result = await session.sendMessage('Summarize the preceding conversation history following your compression rules.');
628
+ const result = await session.sendMessage('Summarize the preceding conversation history following your compression rules.', undefined, abortSignal);
603
629
  return result.response.text() || '';
604
630
  }
605
631
  catch (error) {
632
+ if (abortSignal?.aborted || error?.name === 'AbortError' || error?.message?.includes('abort')) {
633
+ const err = new Error('Operation aborted');
634
+ err.name = 'AbortError';
635
+ throw err;
636
+ }
606
637
  debugLog(`Failed to summarize chat history: ${error?.message || error}`);
607
638
  return '';
608
639
  }
@@ -610,10 +641,12 @@ export async function summarizeChatHistory(history) {
610
641
  /**
611
642
  * Generates a concise title for a chat session based on the user's first message.
612
643
  */
613
- export async function generateChatTitle(firstMessage) {
644
+ export async function generateChatTitle(firstMessage, abortSignal) {
614
645
  const maxLength = 60;
615
646
  if (!firstMessage || firstMessage.trim().length === 0)
616
647
  return 'New Chat';
648
+ if (abortSignal?.aborted)
649
+ return firstMessage.substring(0, maxLength);
617
650
  try {
618
651
  const idToken = await getAuthorizedIdToken();
619
652
  if (!idToken)
@@ -626,13 +659,17 @@ export async function generateChatTitle(firstMessage) {
626
659
  const byokEnabled = await isByokEnabled();
627
660
  let result;
628
661
  if (byokEnabled) {
662
+ const subCheck = await checkByokSubscription();
663
+ if (!subCheck.active) {
664
+ return firstMessage.substring(0, maxLength);
665
+ }
629
666
  const creds = await loadCredentials();
630
667
  result = await proxyClient.generateViaBYOK(creds.geminiApiKey, model, contents, [], // no tools
631
- undefined, instruction, { temperature: 0.2 });
668
+ undefined, instruction, { temperature: 0.2 }, undefined, abortSignal);
632
669
  }
633
670
  else {
634
671
  result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
635
- undefined, instruction, { temperature: 0.2 });
672
+ undefined, instruction, { temperature: 0.2 }, undefined, abortSignal);
636
673
  }
637
674
  let title = '';
638
675
  if (result.parts) {
@@ -1,3 +1,10 @@
1
1
  export declare function login(): Promise<boolean>;
2
2
  export declare function logout(): Promise<void>;
3
3
  export declare function getAuthorizedIdToken(): Promise<string | undefined>;
4
+ /**
5
+ * Checks if the signed-in CLI user has an active $3.99/month BYOK subscription.
6
+ */
7
+ export declare function checkByokSubscription(): Promise<{
8
+ active: boolean;
9
+ message?: string;
10
+ }>;
@@ -160,3 +160,44 @@ export async function getAuthorizedIdToken() {
160
160
  }
161
161
  return undefined;
162
162
  }
163
+ /**
164
+ * Checks if the signed-in CLI user has an active $3.99/month BYOK subscription.
165
+ */
166
+ export async function checkByokSubscription() {
167
+ const idToken = await getAuthorizedIdToken();
168
+ if (!idToken) {
169
+ return {
170
+ active: false,
171
+ message: "Authentication Required: You must be logged into minovative-mind-cli to use BYOK mode. Run 'minovative-mind-cli login' first, then subscribe at https://www.minovativemind.dev/pricing ($3.99/month).",
172
+ };
173
+ }
174
+ try {
175
+ const res = await fetch('https://verifysubscription-6obg3e4zwa-uc.a.run.app', {
176
+ method: 'POST',
177
+ headers: {
178
+ 'Content-Type': 'application/json',
179
+ 'X-Firebase-Auth': `Bearer ${idToken}`,
180
+ },
181
+ });
182
+ if (!res.ok) {
183
+ return {
184
+ active: false,
185
+ message: '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.',
186
+ };
187
+ }
188
+ const data = (await res.json());
189
+ if (!data.hasActiveSubscription) {
190
+ return {
191
+ active: false,
192
+ message: '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.',
193
+ };
194
+ }
195
+ return { active: true };
196
+ }
197
+ catch {
198
+ return {
199
+ active: false,
200
+ message: 'Failed to verify BYOK subscription status. Please check your network connection or visit https://www.minovativemind.dev/pricing to subscribe ($3.99/month).',
201
+ };
202
+ }
203
+ }
@@ -14,8 +14,8 @@ export interface IntentRoute {
14
14
  needsContext: boolean;
15
15
  targetAgent: 'CHAT' | 'EXECUTE';
16
16
  }
17
- export declare function routeIntent(userRequest: string, chatHistory?: string): Promise<IntentRoute>;
18
- export declare function evaluateExecutionComplexity(userRequest: string, investigationSummary: string | undefined, numRelevantFiles: number, chatHistory?: string): Promise<'EASY' | 'HARD'>;
17
+ export declare function routeIntent(userRequest: string, chatHistory?: string, abortSignal?: AbortSignal): Promise<IntentRoute>;
18
+ export declare function evaluateExecutionComplexity(userRequest: string, investigationSummary: string | undefined, numRelevantFiles: number, chatHistory?: string, abortSignal?: AbortSignal): Promise<'EASY' | 'HARD'>;
19
19
  export declare function gatherContext(workspaceRoot: string, userRequest: string, chatHistory: string | undefined, inputHandler: {
20
20
  getAndClear: () => string;
21
21
  waitForPrompt: () => Promise<void>;