minovative-mind-cli 2.2.5 → 2.3.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.
Files changed (35) hide show
  1. package/LICENSE.md +2 -0
  2. package/README.md +8 -8
  3. package/dist/commands/logout.js +1 -1
  4. package/dist/services/agent/slashCommands.js +53 -14
  5. package/dist/services/agent/toolLoop.js +4 -1
  6. package/dist/services/agent/types.d.ts +4 -0
  7. package/dist/services/agent-tools.js +55 -27
  8. package/dist/services/agent.d.ts +4 -0
  9. package/dist/services/agent.js +77 -60
  10. package/dist/services/ai.d.ts +1 -2
  11. package/dist/services/ai.js +21 -19
  12. package/dist/services/auth.d.ts +1 -1
  13. package/dist/services/auth.js +7 -28
  14. package/dist/services/chatHistoryService.d.ts +8 -0
  15. package/dist/services/contextAgent.js +18 -1
  16. package/dist/services/investigationComplexity.d.ts +1 -1
  17. package/dist/services/investigationComplexity.js +1 -1
  18. package/dist/services/orchestration/investigationAgent.d.ts +2 -2
  19. package/dist/services/orchestration/investigationAgent.js +25 -6
  20. package/dist/services/orchestration/orchestrator.d.ts +1 -1
  21. package/dist/services/orchestration/orchestrator.js +37 -18
  22. package/dist/services/orchestration/subAgent.d.ts +1 -1
  23. package/dist/services/orchestration/subAgent.js +23 -13
  24. package/dist/services/proxyClient.d.ts +16 -0
  25. package/dist/services/proxyClient.js +32 -0
  26. package/dist/utils/analysisRunner.js +2 -2
  27. package/dist/utils/config.d.ts +3 -3
  28. package/dist/utils/config.js +3 -3
  29. package/dist/utils/credentialStore.d.ts +30 -0
  30. package/dist/utils/credentialStore.js +540 -0
  31. package/dist/utils/projectStorage.js +70 -0
  32. package/dist/utils/systemPrompts.d.ts +3 -3
  33. package/dist/utils/systemPrompts.js +4 -4
  34. package/oclif.manifest.json +1 -1
  35. package/package.json +2 -1
@@ -27,7 +27,8 @@ const execAsync = promisify(exec);
27
27
  import { debugLog, isDebugOn } from '../utils/logger.js';
28
28
  import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents, } from '../utils/projectStorage.js';
29
29
  import { GEMINI_MODELS } from '../utils/config.js';
30
- import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, setGlobalActiveModel, getGlobalLatestUsageMetadata, } from './ai.js';
30
+ import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, setGlobalActiveModel, } from './ai.js';
31
+ import { getAndResetTurnUsage } from './proxyClient.js';
31
32
  import { changeLogger } from './changeLogger.js';
32
33
  import { chatHistoryService } from './chatHistoryService.js';
33
34
  import { gatherContext, routeIntent, evaluateExecutionComplexity } from './contextAgent.js';
@@ -85,7 +86,7 @@ export async function startAgentLoop(workspaceRoot, version) {
85
86
  chatHistoryService.init(workspaceRoot);
86
87
  const chat = createSharedChatSession();
87
88
  const inputHandler = new AsyncInputHandler();
88
- const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0 };
89
+ const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0 };
89
90
  chat.setSessionInfo(chatSessionState.id, workspaceRoot);
90
91
  const sessionInputHistory = [];
91
92
  let isRawPasteMode = false;
@@ -112,6 +113,8 @@ export async function startAgentLoop(workspaceRoot, version) {
112
113
  chatSessionState.id = crypto.randomUUID();
113
114
  chatSessionState.title = '';
114
115
  chatSessionState.totalTokens = 0;
116
+ chatSessionState.totalInputTokens = 0;
117
+ chatSessionState.totalOutputTokens = 0;
115
118
  chat.setSessionInfo(chatSessionState.id, workspaceRoot);
116
119
  }
117
120
  inputHandler.stop();
@@ -131,15 +134,15 @@ export async function startAgentLoop(workspaceRoot, version) {
131
134
  message: 'Command Menu',
132
135
  options: [
133
136
  { value: '/models', label: '/models', hint: 'Change the active AI model' },
134
- { value: '/plan', label: '/plan', hint: 'Toggle plan mode (build a plan without executing)' },
137
+ { value: '/plan', label: '/plan', hint: `Toggle plan mode (build a plan without executing) ${isPlanMode ? pc.green('(ON)') : pc.red('(OFF)')}` },
135
138
  { value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
136
139
  { value: '/clear', label: '/clear', hint: 'Clear chat session history' },
137
- { value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
138
- { value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
140
+ { value: '/debug', label: '/debug', hint: `Toggle internal debug logs ${isDebugOn() ? pc.green('(ON)') : pc.red('(OFF)')}` },
141
+ { value: '/auto-approve', label: '/auto-approve', hint: `Approve all future terminal commands ${getApprovalMode() === 'skip-all' ? pc.green('(ON)') : pc.red('(OFF)')}` },
139
142
  {
140
143
  value: '/sub-agents',
141
144
  label: '/sub-agents',
142
- hint: 'Toggle the MMAAK Engine for parallel investigation and execution',
145
+ hint: `Toggle the MMAAK Engine for parallel investigation and execution ${isSubAgentsEnabled() ? pc.green('(ON)') : pc.red('(OFF)')}`,
143
146
  },
144
147
  {
145
148
  value: '/workspaces',
@@ -311,9 +314,9 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
311
314
  // Apply the dynamic prompt updates and tool registrations to the active chat session
312
315
  chat.setAgentConfig(dynamicSystemInstruction, config.tools);
313
316
  if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
314
- let selectedModel = GEMINI_MODELS.FLASH_3_5;
317
+ let selectedModel = GEMINI_MODELS.FLASH;
315
318
  if (effectiveTargetAgent === 'CHAT') {
316
- selectedModel = GEMINI_MODELS.FLASH_LITE_3_1;
319
+ selectedModel = GEMINI_MODELS.FLASH_LITE;
317
320
  }
318
321
  else {
319
322
  spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
@@ -321,7 +324,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
321
324
  const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
322
325
  spinner.stop();
323
326
  process.stdout.write('\x1b[2K\r');
324
- selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE_3_1 : GEMINI_MODELS.FLASH_3_5;
327
+ selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE : GEMINI_MODELS.FLASH;
325
328
  }
326
329
  chat.setModel(selectedModel);
327
330
  debugLog(`Auto-routing to model ${selectedModel} based on intent ${effectiveTargetAgent}`);
@@ -330,21 +333,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
330
333
  chat.setModel(getGlobalActiveModel());
331
334
  }
332
335
  if (gatherRes.contextResult) {
333
- if (!gatherRes.contextResult.isParallel) {
334
- if (!inputHandler.isCurrentlyPrompting()) {
335
- spinner.stop(pc.green('🔍 Investigation complete.'));
336
- spinner.start('Thinking...');
337
- }
338
- else {
339
- p.log.success(pc.green('🔍 Investigation complete.'));
340
- }
336
+ if (!inputHandler.isCurrentlyPrompting()) {
337
+ spinner.stop(pc.green('🔍 Investigation complete.'));
338
+ spinner.start('Thinking...');
341
339
  }
342
340
  else {
343
- // Parallel investigation already printed its own detailed summary log
344
- if (!inputHandler.isCurrentlyPrompting()) {
345
- spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
346
- spinner.start('Thinking...');
347
- }
341
+ p.log.success(pc.green('🔍 Investigation complete.'));
348
342
  }
349
343
  }
350
344
  else {
@@ -352,6 +346,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
352
346
  spinner.message('Thinking...');
353
347
  }
354
348
  }
349
+ let gitBranch = '';
350
+ try {
351
+ const { stdout } = await execAsync('git branch --show-current', { cwd: workspaceRoot });
352
+ gitBranch = stdout.trim();
353
+ }
354
+ catch { }
355
355
  if (!isPlanMode && effectiveTargetAgent === 'EXECUTE' && isSubAgentsEnabled()) {
356
356
  spinner.stop(); // Clear the spinner before delegating
357
357
  process.stdout.write('\x1b[2K\r');
@@ -376,16 +376,19 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
376
376
  console.log(marked.parse(cleanText));
377
377
  }
378
378
  // Print Usage Stats
379
- const usage = getGlobalLatestUsageMetadata();
380
- if (usage) {
381
- if (usage.cachedTokens > 0) {
382
- const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
383
- const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
384
- p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
385
- }
386
- if (usage.remainingBalance !== undefined) {
387
- p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
388
- }
379
+ const usage = getAndResetTurnUsage();
380
+ chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
381
+ chatSessionState.latestUsageMetadata = usage;
382
+ chatSessionState.totalTokens += usage.totalTokenCount;
383
+ chatSessionState.totalInputTokens += (usage.promptTokens || 0) + (usage.cachedTokens || 0);
384
+ chatSessionState.totalOutputTokens += usage.candidatesTokens || 0;
385
+ if (usage.cachedTokens && usage.cachedTokens > 0) {
386
+ const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
387
+ const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
388
+ p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
389
+ }
390
+ if (usage.remainingBalance !== undefined) {
391
+ p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
389
392
  }
390
393
  // Post-execution steps for the UI (saving history, invalidate cache)
391
394
  const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
@@ -419,6 +422,17 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
419
422
  title: chatSessionState.title,
420
423
  timestamp: Date.now(),
421
424
  history,
425
+ creditsRemaining: usage.remainingBalance,
426
+ lastTurnDuration: turnDuration,
427
+ modelName: chat.getModel(),
428
+ totalTokens: chatSessionState.totalTokens,
429
+ totalInputTokens: chatSessionState.totalInputTokens,
430
+ totalOutputTokens: chatSessionState.totalOutputTokens,
431
+ gitBranch: gitBranch || undefined,
432
+ autoApprove: getApprovalMode() === 'skip-all',
433
+ subAgents: isSubAgentsEnabled(),
434
+ latestUsageMetadata: chatSessionState.latestUsageMetadata,
435
+ previousUsageMetadata: chatSessionState.previousUsageMetadata,
422
436
  })
423
437
  .catch((e) => debugLog('Failed to auto-save session: ' + e));
424
438
  }
@@ -458,7 +472,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
458
472
  const correctionRes = await executeSelfCorrectionLoop(chat, result, workspaceRoot, inputHandler, effectiveTargetAgent, ac.signal, spinner, userInput, isPlanMode);
459
473
  const finalText = correctionRes.finalText;
460
474
  // Update latest usage metadata to reflect all completed turns
461
- latestUsage = chat.getLatestUsageMetadata() || latestUsage;
475
+ const usage = getAndResetTurnUsage();
462
476
  const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
463
477
  const modifiedFiles = postExecutionChanges
464
478
  .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
@@ -475,18 +489,18 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
475
489
  console.log(marked.parse(cleanText));
476
490
  }
477
491
  }
478
- if (latestUsage) {
479
- if (latestUsage.cachedTokens && latestUsage.cachedTokens > 0) {
480
- const totalInputTokens = (latestUsage.promptTokens || 0) + latestUsage.cachedTokens;
481
- const percentSaved = totalInputTokens > 0 ? Math.round((latestUsage.cachedTokens / totalInputTokens) * 100) : 0;
482
- p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(latestUsage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
492
+ if (usage) {
493
+ if (usage.cachedTokens && usage.cachedTokens > 0) {
494
+ const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
495
+ const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
496
+ p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
483
497
  }
484
- const inputTokens = (latestUsage.promptTokens || 0) + (latestUsage.cachedTokens || 0);
485
- const outputTokens = latestUsage.candidatesTokens || 0;
486
- const totalTokens = latestUsage.totalTokenCount || inputTokens + outputTokens;
498
+ const inputTokens = (usage.promptTokens || 0) + (usage.cachedTokens || 0);
499
+ const outputTokens = usage.candidatesTokens || 0;
500
+ const totalTokens = inputTokens + outputTokens;
487
501
  p.log.info(`${pc.dim('Tokens Used:')} ${pc.cyan(totalTokens.toLocaleString())} ${pc.dim(`(Input: ${inputTokens.toLocaleString()}, Output: ${outputTokens.toLocaleString()})`)}`);
488
- if (latestUsage.remainingBalance !== undefined) {
489
- p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);
502
+ if (usage.remainingBalance !== undefined) {
503
+ p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
490
504
  }
491
505
  }
492
506
  const turnEndTime = Date.now();
@@ -496,18 +510,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
496
510
  if (finalText !== '[Generation stopped.]') {
497
511
  changeLogger.markComplete();
498
512
  }
499
- let turnTokens = 0;
500
- if (latestUsage) {
501
- turnTokens =
502
- (latestUsage.promptTokens || 0) + (latestUsage.cachedTokens || 0) + (latestUsage.candidatesTokens || 0);
513
+ if (usage) {
514
+ chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
515
+ chatSessionState.latestUsageMetadata = usage;
516
+ chatSessionState.totalTokens += usage.totalTokenCount || 0;
517
+ chatSessionState.totalInputTokens += (usage.promptTokens || 0) + (usage.cachedTokens || 0);
518
+ chatSessionState.totalOutputTokens += usage.candidatesTokens || 0;
503
519
  }
504
- chatSessionState.totalTokens += turnTokens;
505
- let gitBranch = '';
506
- try {
507
- const { stdout } = await execAsync('git branch --show-current', { cwd: workspaceRoot });
508
- gitBranch = stdout.trim();
509
- }
510
- catch { }
511
520
  // Auto-save chat history
512
521
  const history = chat.getRawHistory();
513
522
  if (history.length > 0) {
@@ -525,9 +534,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
525
534
  lastTurnDuration: turnDuration,
526
535
  modelName: chat.getModel(),
527
536
  totalTokens: chatSessionState.totalTokens,
537
+ totalInputTokens: chatSessionState.totalInputTokens,
538
+ totalOutputTokens: chatSessionState.totalOutputTokens,
528
539
  gitBranch: gitBranch || undefined,
529
540
  autoApprove: getApprovalMode() === 'skip-all',
530
541
  subAgents: isSubAgentsEnabled(),
542
+ latestUsageMetadata: chatSessionState.latestUsageMetadata,
543
+ previousUsageMetadata: chatSessionState.previousUsageMetadata,
531
544
  });
532
545
  }
533
546
  catch (e) {
@@ -544,9 +557,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
544
557
  lastTurnDuration: turnDuration,
545
558
  modelName: chat.getModel(),
546
559
  totalTokens: chatSessionState.totalTokens,
560
+ totalInputTokens: chatSessionState.totalInputTokens,
561
+ totalOutputTokens: chatSessionState.totalOutputTokens,
547
562
  gitBranch: gitBranch || undefined,
548
563
  autoApprove: getApprovalMode() === 'skip-all',
549
564
  subAgents: isSubAgentsEnabled(),
565
+ latestUsageMetadata: chatSessionState.latestUsageMetadata,
566
+ previousUsageMetadata: chatSessionState.previousUsageMetadata,
550
567
  })
551
568
  .catch((e) => debugLog('Failed to auto-save session: ' + e));
552
569
  }
@@ -584,19 +601,19 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
584
601
  hint: 'The Pro model for complex logic',
585
602
  },
586
603
  {
587
- value: 'gemini-3.5-flash',
588
- label: 'Gemini 3.5 Flash',
604
+ value: 'gemini-3.6-flash',
605
+ label: 'Gemini 3.6 Flash',
589
606
  hint: 'Balanced performance',
590
607
  },
591
608
  {
592
- value: 'gemini-3.1-flash-lite',
593
- label: 'Gemini 3.1 Flash-Lite',
594
- hint: 'Ultra-fast and cost-effective',
609
+ value: 'gemini-3.5-flash-lite',
610
+ label: 'Gemini 3.5 Flash-Lite',
611
+ hint: 'Best for speed and cost efficiency',
595
612
  },
596
613
  {
597
614
  value: 'auto',
598
615
  label: 'Auto (Flash-Lite / Flash)',
599
- hint: 'Dynamically routes between Gemini 3.1 Flash-Lite and Gemini 3.5 based on prompt complexity',
616
+ hint: 'Dynamically routes between Gemini 3.5 Flash-Lite and Gemini 3.6 based on prompt complexity',
600
617
  },
601
618
  ],
602
619
  });
@@ -3,7 +3,6 @@ export declare function setModelOverride(agent: 'context' | 'execution', overrid
3
3
  export declare function clearModelOverrides(): void;
4
4
  export declare function setGlobalActiveModel(model: string): void;
5
5
  export declare function getGlobalActiveModel(): string;
6
- export declare function getGlobalLatestUsageMetadata(): any;
7
6
  export declare class ProxyChatSession {
8
7
  private history;
9
8
  private modelName;
@@ -74,7 +73,7 @@ export declare function getPlanModeConfig(): {
74
73
  tools: never[];
75
74
  };
76
75
  /**
77
- * Compresses a large string of text using gemini-3.1-flash-lite.
76
+ * Compresses a large string of text using gemini-3.5-flash-lite.
78
77
  * Used for shrinking context payloads to prevent OOM/choking.
79
78
  */
80
79
  export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any): Promise<string>;
@@ -52,10 +52,6 @@ export function setGlobalActiveModel(model) {
52
52
  export function getGlobalActiveModel() {
53
53
  return globalActiveModel;
54
54
  }
55
- let globalLatestUsageMetadata = undefined;
56
- export function getGlobalLatestUsageMetadata() {
57
- return globalLatestUsageMetadata;
58
- }
59
55
  const proxyClient = new ProxyClient();
60
56
  // ─── History Limits ──────────────────────────────────────────────────
61
57
  /** Maximum number of Content entries to keep in the sliding history window. */
@@ -214,9 +210,6 @@ export class ProxyChatSession {
214
210
  this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
215
211
  abortSignal);
216
212
  this.latestUsageMetadata = result.usageMetadata;
217
- if (result.usageMetadata) {
218
- globalLatestUsageMetadata = result.usageMetadata;
219
- }
220
213
  // Track token usage metrics
221
214
  if (result.usageMetadata) {
222
215
  const collector = getMetricCollector();
@@ -283,7 +276,7 @@ export class ProxyChatSession {
283
276
  export function createSharedChatSession() {
284
277
  let model = executionModelOverride || getGlobalActiveModel();
285
278
  if (model === 'auto')
286
- model = GEMINI_MODELS.FLASH_3_5; // Will be overridden per-turn in executeSingleTurn
279
+ model = GEMINI_MODELS.FLASH; // Will be overridden per-turn in executeSingleTurn
287
280
  return new ProxyChatSession(model, GENERAL_CHAT_INSTRUCTION, [], {
288
281
  maxOutputTokens: MAX_OUTPUT_TOKENS,
289
282
  temperature: executionTempOverride !== null ? executionTempOverride : 1,
@@ -310,10 +303,10 @@ export function getPlanModeConfig() {
310
303
  };
311
304
  }
312
305
  /**
313
- * Compresses a large string of text using gemini-3.1-flash-lite.
306
+ * Compresses a large string of text using gemini-3.5-flash-lite.
314
307
  * Used for shrinking context payloads to prevent OOM/choking.
315
308
  */
316
- export async function compressTextUsingFlashLite(text, instruction = 'Summarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Keep it under 1500 characters.', inlineData) {
309
+ 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) {
317
310
  if (!text || (text.length < 1000 && !inlineData))
318
311
  return text; // Don't compress tiny texts
319
312
  try {
@@ -322,7 +315,7 @@ export async function compressTextUsingFlashLite(text, instruction = 'Summarize
322
315
  return text;
323
316
  let model = getGlobalActiveModel();
324
317
  if (model === 'auto')
325
- model = GEMINI_MODELS.FLASH_LITE_3_1;
318
+ model = GEMINI_MODELS.FLASH_LITE;
326
319
  const parts = [{ text }];
327
320
  if (inlineData) {
328
321
  parts.push({ inlineData });
@@ -461,8 +454,17 @@ export function getContextToolDeclarations() {
461
454
  description: 'Write and execute a disposable analysis script to structurally map code in the workspace. ' +
462
455
  'Use this to get exact line ranges for functions, classes, and variables by leveraging the ' +
463
456
  "language's native AST parser (e.g., TypeScript compiler API, Python ast module, go/parser). " +
457
+ 'You can also use this to probe the development environment — detecting available runtimes ' +
458
+ '(e.g., node --version, python3 --version), checking if ports are in use, identifying project ' +
459
+ 'type (monorepo, package manager), or diagnosing system-level issues (disk space, memory) that ' +
460
+ 'may affect execution. ' +
461
+ 'For complex investigation tasks, you can write lightweight ML scripts (e.g., TF-IDF cosine similarity ' +
462
+ 'to rank file relevance, Z-score outlier detection for anomalous log lines, K-Means clustering, or Naive ' +
463
+ 'Bayes classification). Default to "node" for generic math/analysis as a safe baseline, but act like ' +
464
+ 'a native inhabitant of the host environment — if Python, Go, Rust, or specialized libraries are available ' +
465
+ 'in the project context, leverage the host\'s native runtimes and standard libraries for maximum efficiency. ' +
464
466
  'The script is executed from a temporary directory and automatically cleaned up after execution. ' +
465
- 'Output should be structured JSON to stdout with name, type, startLine, endLine for each code element. ' +
467
+ 'Output should be structured JSON to stdout. ' +
466
468
  'Use the results to make precise read_file calls with exact startLine/endLine instead of guessing. ' +
467
469
  'CRITICAL: Do not use this tool on binary, document, or non-code files (e.g. PDF, image, audio, docx).',
468
470
  parameters: {
@@ -519,7 +521,7 @@ export function createContextAgentSession() {
519
521
  const contextTools = getContextToolDeclarations();
520
522
  let model = contextModelOverride || getGlobalActiveModel();
521
523
  if (model === 'auto')
522
- model = GEMINI_MODELS.FLASH_3_5;
524
+ model = GEMINI_MODELS.FLASH;
523
525
  return new ProxyChatSession(model, CONTEXT_SYSTEM_INSTRUCTION.replace('{{MULTI_WORKSPACE_BLOCK}}', getMultiWorkspaceBlock()), contextTools, {
524
526
  maxOutputTokens: MAX_OUTPUT_TOKENS,
525
527
  temperature: contextTempOverride !== null ? contextTempOverride : 1,
@@ -531,7 +533,7 @@ export function createContextAgentSession() {
531
533
  export function createIntentRouterSession() {
532
534
  let model = getGlobalActiveModel();
533
535
  if (model === 'auto')
534
- model = GEMINI_MODELS.FLASH_LITE_3_1;
536
+ model = GEMINI_MODELS.FLASH_LITE;
535
537
  return new ProxyChatSession(model, INTENT_ROUTER_SYSTEM_INSTRUCTION, [], // no tools
536
538
  { temperature: 0, responseMimeType: 'application/json' });
537
539
  }
@@ -539,7 +541,7 @@ export function createIntentRouterSession() {
539
541
  export function createExecutionComplexitySession() {
540
542
  let model = getGlobalActiveModel();
541
543
  if (model === 'auto')
542
- model = GEMINI_MODELS.FLASH_LITE_3_1;
544
+ model = GEMINI_MODELS.FLASH_LITE;
543
545
  return new ProxyChatSession(model, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
544
546
  { temperature: 0, responseMimeType: 'application/json' });
545
547
  }
@@ -547,7 +549,7 @@ export function createExecutionComplexitySession() {
547
549
  export function createInvestigationComplexitySession() {
548
550
  let model = getGlobalActiveModel();
549
551
  if (model === 'auto')
550
- model = GEMINI_MODELS.FLASH_LITE_3_1;
552
+ model = GEMINI_MODELS.FLASH_LITE;
551
553
  return new ProxyChatSession(model, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
552
554
  { temperature: 0, responseMimeType: 'application/json' });
553
555
  }
@@ -555,7 +557,7 @@ export function createInvestigationComplexitySession() {
555
557
  export function createWebSearchAgentSession() {
556
558
  let model = getGlobalActiveModel();
557
559
  if (model === 'auto')
558
- model = GEMINI_MODELS.FLASH_3_5;
560
+ model = GEMINI_MODELS.FLASH;
559
561
  return new ProxyChatSession(model, WEB_SEARCH_SYSTEM_INSTRUCTION, [{ googleSearch: {} }], {
560
562
  maxOutputTokens: MAX_OUTPUT_TOKENS,
561
563
  temperature: 1,
@@ -574,11 +576,11 @@ export async function generateChatTitle(firstMessage) {
574
576
  const idToken = await getAuthorizedIdToken();
575
577
  if (!idToken)
576
578
  return firstMessage.substring(0, maxLength);
577
- const instruction = "You are a helpful assistant that generates extremely concise chat titles based on a user's first message. Output ONLY the title, no quotes, no markdown, no punctuation.";
579
+ const instruction = "<identity>\nYou are a helpful assistant that generates extremely concise chat titles based on a user's first message.\n</identity>\n\n<directives>\nOutput ONLY the title, no quotes, no markdown, no punctuation.\n</directives>";
578
580
  const contents = [{ role: 'user', parts: [{ text: firstMessage.substring(0, 500) }] }];
579
581
  let model = getGlobalActiveModel();
580
582
  if (model === 'auto')
581
- model = GEMINI_MODELS.FLASH_LITE_3_1;
583
+ model = GEMINI_MODELS.FLASH_LITE;
582
584
  const result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
583
585
  undefined, instruction, { temperature: 0.2 });
584
586
  let title = '';
@@ -1,3 +1,3 @@
1
1
  export declare function login(): Promise<boolean>;
2
- export declare function logout(): void;
2
+ export declare function logout(): Promise<void>;
3
3
  export declare function getAuthorizedIdToken(): Promise<string | undefined>;
@@ -1,23 +1,5 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
1
  import { FIREBASE_API_KEY, GITHUB_CLIENT_ID } from '../utils/config.js';
5
- const CONFIG_FILE = path.join(os.homedir(), '.minovative-mind-cli.json');
6
- function getAuthData() {
7
- try {
8
- if (fs.existsSync(CONFIG_FILE)) {
9
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
10
- }
11
- }
12
- catch (err) {
13
- // ignore
14
- }
15
- return {};
16
- }
17
- function saveAuthData(data) {
18
- const current = getAuthData();
19
- fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
20
- }
2
+ import { saveCredentials, loadCredentials, clearCredentials } from '../utils/credentialStore.js';
21
3
  export async function login() {
22
4
  try {
23
5
  // 1. Start GitHub Device Flow
@@ -98,8 +80,8 @@ export async function login() {
98
80
  if (!firebaseTokenResponse.idToken) {
99
81
  throw new Error('Failed to get Firebase ID token.');
100
82
  }
101
- // 5. Store the Firebase token securely
102
- saveAuthData({
83
+ // 5. Store the Firebase token in the OS-native secure credential store
84
+ await saveCredentials({
103
85
  idToken: firebaseTokenResponse.idToken,
104
86
  refreshToken: firebaseTokenResponse.refreshToken,
105
87
  idTokenExpiry: Date.now() + 50 * 60 * 1000, // 50 mins
@@ -139,14 +121,11 @@ async function exchangeGithubTokenForFirebase(githubAccessToken) {
139
121
  }
140
122
  return await signInResponse.json();
141
123
  }
142
- export function logout() {
143
- if (fs.existsSync(CONFIG_FILE)) {
144
- fs.unlinkSync(CONFIG_FILE);
145
- }
146
- console.log('Successfully signed out.');
124
+ export async function logout() {
125
+ await clearCredentials();
147
126
  }
148
127
  export async function getAuthorizedIdToken() {
149
- const data = getAuthData();
128
+ const data = await loadCredentials();
150
129
  if (!data.idToken)
151
130
  return undefined;
152
131
  if (data.idTokenExpiry && Date.now() < data.idTokenExpiry) {
@@ -168,7 +147,7 @@ export async function getAuthorizedIdToken() {
168
147
  const refreshData = (await response.json());
169
148
  const newIdToken = refreshData.id_token;
170
149
  if (newIdToken) {
171
- saveAuthData({
150
+ await saveCredentials({
172
151
  idToken: newIdToken,
173
152
  refreshToken: refreshData.refresh_token || data.refreshToken,
174
153
  idTokenExpiry: Date.now() + 50 * 60 * 1000,
@@ -21,12 +21,20 @@ export interface ChatSessionData {
21
21
  modelName?: string;
22
22
  /** Optional total cumulative tokens consumed during the session. */
23
23
  totalTokens?: number;
24
+ /** Optional total cumulative input tokens (prompt + cached) consumed during the session. */
25
+ totalInputTokens?: number;
26
+ /** Optional total cumulative output tokens consumed during the session. */
27
+ totalOutputTokens?: number;
24
28
  /** Optional name of the Git branch active when this session was last updated. */
25
29
  gitBranch?: string;
26
30
  /** Optional flag indicating whether auto-approval of commands was enabled in this session. */
27
31
  autoApprove?: boolean;
28
32
  /** Optional flag indicating whether sub-agents were enabled or active during this session. */
29
33
  subAgents?: boolean;
34
+ /** Optional metadata about the token usage from the very last turn. */
35
+ latestUsageMetadata?: any;
36
+ /** Optional metadata about the token usage from the turn prior to the last turn. */
37
+ previousUsageMetadata?: any;
30
38
  }
31
39
  /**
32
40
  * Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
@@ -232,6 +232,8 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
232
232
  let summary = 'No relevant context found.';
233
233
  let isInvestigationFinished = false;
234
234
  const visitedToolCalls = new Set();
235
+ let consecutiveDuplicates = 0;
236
+ const MAX_CONSECUTIVE_DUPLICATES = 3;
235
237
  let chainedMessages = [];
236
238
  let webSearchSummary = '';
237
239
  // Initial prompt
@@ -286,14 +288,29 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
286
288
  // Deduplicate identical tool calls
287
289
  const callSignature = `${call.name}:${JSON.stringify(args)}`;
288
290
  if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
291
+ consecutiveDuplicates++;
292
+ if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
293
+ // Force-finish: the model is stuck in a loop, finalize with whatever context we have
294
+ const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
295
+ debugLog(`[Context Agent] ${forceMsg}`);
296
+ if (onProgress)
297
+ onProgress(forceMsg);
298
+ if (!summary || summary === 'No relevant context found.') {
299
+ summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
300
+ }
301
+ isFinished = true;
302
+ isInvestigationFinished = relevantFiles.size > 0;
303
+ break;
304
+ }
289
305
  functionResponses.push({
290
306
  functionResponse: {
291
307
  name: call.name,
292
- response: { error: 'You have already made this exact tool call previously. Please review your context history or try a different action.' }
308
+ response: { error: `DUPLICATE CALL BLOCKED (attempt ${consecutiveDuplicates}/${MAX_CONSECUTIVE_DUPLICATES}): You already executed this exact tool call. Do NOT retry it. Use the results you already have and call finish_investigation now, or try a DIFFERENT tool call with different parameters.` }
293
309
  }
294
310
  });
295
311
  continue;
296
312
  }
313
+ consecutiveDuplicates = 0;
297
314
  visitedToolCalls.add(callSignature);
298
315
  let logMsg = ` [Context Agent] Executing ${call.name}`;
299
316
  if (call.name === 'finish_investigation') {
@@ -31,7 +31,7 @@ export interface InvestigationComplexityResult {
31
31
  /**
32
32
  * Evaluates whether the investigation phase should be parallelized.
33
33
  *
34
- * Uses `gemini-3.1-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
34
+ * Uses `gemini-3.5-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
35
35
  * investigation complexity. The PM dynamically identifies domains and groups
36
36
  * them into agent assignments. Agent count = `agentAssignments.length`, which
37
37
  * may be fewer than `domains.length` when related domains are batched together.
@@ -16,7 +16,7 @@ import { debugLog } from '../utils/logger.js';
16
16
  /**
17
17
  * Evaluates whether the investigation phase should be parallelized.
18
18
  *
19
- * Uses `gemini-3.1-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
19
+ * Uses `gemini-3.5-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
20
20
  * investigation complexity. The PM dynamically identifies domains and groups
21
21
  * them into agent assignments. Agent count = `agentAssignments.length`, which
22
22
  * may be fewer than `domains.length` when related domains are batched together.
@@ -59,8 +59,8 @@ export declare class InvestigationAgentRunner {
59
59
  private creditsUsed;
60
60
  private inputTokens;
61
61
  private outputTokens;
62
- /** Shorter stall timeout for investigation agents (120s to allow for heavy vision/web tasks) */
63
- static readonly STALL_TIMEOUT_MS = 120000;
62
+ /** Stall timeout for investigation agents (300s to allow for heavy vision/web tasks) */
63
+ static readonly STALL_TIMEOUT_MS = 300000;
64
64
  constructor(agentLabel: string, domains: string[], workspaceRoot: string, readCache: ReadCache, projectTree: string, projectType: string);
65
65
  /**
66
66
  * Builds a domain-scoped system instruction. Extends the base Context Agent