minovative-mind-cli 2.3.3 → 2.5.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 (38) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/chat.js +2 -1
  3. package/dist/services/agent/slashCommands.js +152 -2
  4. package/dist/services/agent/toolLoop.js +5 -17
  5. package/dist/services/agent-tools.js +26 -14
  6. package/dist/services/agent.js +43 -13
  7. package/dist/services/ai.d.ts +2 -2
  8. package/dist/services/ai.js +131 -14
  9. package/dist/services/changeLogger.js +2 -2
  10. package/dist/services/chatHistoryService.d.ts +8 -0
  11. package/dist/services/chatHistoryService.js +24 -11
  12. package/dist/services/contextAgent.d.ts +1 -0
  13. package/dist/services/contextAgent.js +57 -32
  14. package/dist/services/metrics.d.ts +21 -4
  15. package/dist/services/metrics.js +18 -0
  16. package/dist/services/orchestration/investigationAgent.js +0 -32
  17. package/dist/services/orchestration/investigationCache.d.ts +25 -0
  18. package/dist/services/orchestration/investigationCache.js +135 -0
  19. package/dist/services/orchestration/investigationOrchestrator.js +5 -1
  20. package/dist/services/orchestration/messageBus.d.ts +1 -1
  21. package/dist/services/orchestration/messageBus.js +14 -14
  22. package/dist/services/orchestration/orchestrator.js +1 -1
  23. package/dist/services/orchestration/readCache.js +4 -0
  24. package/dist/services/orchestration/scopedTools.js +2 -1
  25. package/dist/services/orchestration/subAgent.js +10 -26
  26. package/dist/services/proxyClient.d.ts +8 -0
  27. package/dist/services/proxyClient.js +17 -0
  28. package/dist/services/verificationService.js +15 -6
  29. package/dist/utils/config.d.ts +4 -0
  30. package/dist/utils/config.js +8 -0
  31. package/dist/utils/credentialStore.d.ts +7 -0
  32. package/dist/utils/credentialStore.js +8 -0
  33. package/dist/utils/projectStorage.js +7 -0
  34. package/dist/utils/syntaxValidator.js +125 -21
  35. package/dist/utils/systemPrompts.d.ts +2 -2
  36. package/dist/utils/systemPrompts.js +3 -3
  37. package/oclif.manifest.json +2 -2
  38. package/package.json +1 -2
package/README.md CHANGED
@@ -93,6 +93,11 @@ Hot-swap during a session using `/models`:
93
93
  | **Gemini 3.1 Pro** | Complex architectural changes |
94
94
  | **Gemini 3.5 Flash Lite** | Best for speed and cost efficiency |
95
95
 
96
+ ### BYOK (Bring Your Own Key)
97
+ If you prefer to use your own Gemini API key, you can configure it via the settings.
98
+ * **Configuration:** Set `useByok: true` and `geminiApiKey: "YOUR_KEY"` in your credentials storage.
99
+ * **Error Handling:** If your key is invalid, expired, or you hit rate limits, the CLI will report a `BYOK AI Error`. Please check your API key status in the Google AI Studio dashboard.
100
+
96
101
  > Background tasks (routing, compression, commits) always use lightweight
97
102
  > models automatically. You pay for those lightweight model
98
103
  > background ai models and for your selected model
@@ -30,10 +30,11 @@ Inside the chat session, you can use the following commands in the slash menu:
30
30
  /auto-approve - Toggle automatic approval of tool/command runs
31
31
  /sub-agents - Toggle the MMAAK Engine for parallel investigation and execution
32
32
  /workspaces - Manage external workspaces for cross-project development
33
+ /config-key - BYOK (Bring Your Own Key) Configuration
33
34
  /stats - View current session statistics and configuration
34
35
  /commit - Commit current workspace changes to Git
35
36
  /revert - Revert the last file modification made by the agent
36
- /chats - View, resume, or delete previous chat sessions
37
+ /chats - View, resume, or delete previous chat sessions (includes bulk delete)
37
38
 
38
39
  Chat Controls:
39
40
  - Multi-line Input: End a line with \\ to continue on the next line
@@ -11,8 +11,11 @@ import { chatHistoryService } from '../chatHistoryService.js';
11
11
  import { printLogo, brandBg, brandFg } from '../../utils/logo.js';
12
12
  import { readPaste } from '../../utils/paste.js';
13
13
  import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled } from '../agent-tools.js';
14
- import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel } from '../ai.js';
15
- import { GEMINI_MODELS } from '../../utils/config.js';
14
+ import { ProxyClient, getAndResetTurnUsage } from '../proxyClient.js';
15
+ import { getAuthorizedIdToken } from '../auth.js';
16
+ import { GEMINI_MODELS, isByokEnabled } from '../../utils/config.js';
17
+ import { loadCredentials, updateCredentialField } from '../../utils/credentialStore.js';
18
+ import { getGlobalActiveModel, setGlobalActiveModel, ProxyChatSession } from '../ai.js';
16
19
  const execAsync = promisify(exec);
17
20
  /**
18
21
  * Handles all slash command operations (/paste, /clear, /models, /debug, /auto-approve, /revert, /commit).
@@ -21,6 +24,44 @@ const execAsync = promisify(exec);
21
24
  export async function handleSlashCommand(command, context) {
22
25
  const { chat, inputHandler, workspaceRoot, version, chatSessionState } = context;
23
26
  const lowerCommand = command.toLowerCase();
27
+ if (lowerCommand === '/config-key') {
28
+ const creds = await loadCredentials();
29
+ const action = await p['select']({
30
+ message: 'BYOK (Bring Your Own Key) Configuration',
31
+ options: [
32
+ { value: 'status', label: 'Status' },
33
+ { value: 'toggle', label: creds.useByok ? 'Disable BYOK' : 'Enable BYOK' },
34
+ { value: 'set', label: 'Set API Key' },
35
+ { value: 'clear', label: 'Clear API Key' },
36
+ { value: 'cancel', label: 'Cancel' },
37
+ ],
38
+ });
39
+ if (action === 'status') {
40
+ const enabled = await isByokEnabled();
41
+ p.log.info(`BYOK Status: ${enabled ? pc.green('Enabled') : pc.red('Disabled')}\n` +
42
+ `API Key: ${creds.geminiApiKey ? pc.green('Set') : pc.red('Not Set')}`);
43
+ }
44
+ else if (action === 'toggle') {
45
+ await updateCredentialField('useByok', !creds.useByok);
46
+ p.log.success(`BYOK ${!creds.useByok ? 'enabled' : 'disabled'}.`);
47
+ }
48
+ else if (action === 'set') {
49
+ const key = await p['text']({
50
+ message: 'Enter your Gemini API Key:',
51
+ validate: (value) => (value ? undefined : 'API Key is required'),
52
+ });
53
+ if (key && typeof key === 'string') {
54
+ await updateCredentialField('geminiApiKey', key);
55
+ p.log.success('API Key updated.');
56
+ }
57
+ }
58
+ else if (action === 'clear') {
59
+ await updateCredentialField('geminiApiKey', '');
60
+ await updateCredentialField('useByok', false);
61
+ p.log.success('API Key cleared and BYOK disabled.');
62
+ }
63
+ return { shouldContinue: true };
64
+ }
24
65
  if (lowerCommand === '/paste') {
25
66
  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)'));
26
67
  try {
@@ -155,14 +196,37 @@ export async function handleSlashCommand(command, context) {
155
196
  const autoApprove = getApprovalMode() === 'skip-all' ? 'Enabled' : 'Disabled';
156
197
  const subAgents = isSubAgentsEnabled() ? 'Enabled' : 'Disabled';
157
198
  const planMode = context.isPlanMode ? 'Enabled' : 'Disabled';
199
+ const byokEnabled = await isByokEnabled() ? 'Enabled' : 'Disabled';
158
200
  p.log.step(pc.magenta('📊 Session Statistics & Status'));
159
201
  console.log(pc.dim('----------------------------------------'));
160
202
  console.log(`${pc.bold('AI Model:')} ${pc.cyan(displayModel)}`);
161
203
  console.log(`${pc.bold('Auto-Approve:')} ${autoApprove === 'Enabled' ? pc.green(autoApprove) : pc.yellow(autoApprove)}`);
162
204
  console.log(`${pc.bold('Sub-Agents:')} ${subAgents === 'Enabled' ? pc.green(subAgents) : pc.yellow(subAgents)}`);
163
205
  console.log(`${pc.bold('Plan Mode:')} ${planMode === 'Enabled' ? pc.green(planMode) : pc.yellow(planMode)}`);
206
+ console.log(`${pc.bold('BYOK:')} ${byokEnabled === 'Enabled' ? pc.green(byokEnabled) : pc.yellow(byokEnabled)}`);
164
207
  const debugMode = isDebugOn() ? 'Enabled' : 'Disabled';
165
208
  console.log(`${pc.bold('Debug Log:')} ${debugMode === 'Enabled' ? pc.green(debugMode) : pc.yellow(debugMode)}`);
209
+ try {
210
+ const { getInvestigationCacheStats } = await import('../orchestration/investigationCache.js');
211
+ const invStats = getInvestigationCacheStats(workspaceRoot);
212
+ const memBankSizeKB = (invStats.sizeBytes / 1024).toFixed(1);
213
+ console.log(`${pc.bold('Memory Bank:')} ${pc.cyan(`${invStats.entries} cached investigations (${memBankSizeKB} KB / 5 MB)`)}`);
214
+ const { readCache } = await import('../../utils/projectStorage.js');
215
+ const contextCache = readCache(workspaceRoot, 'context_cache.json');
216
+ const contextEntries = contextCache ? Object.keys(contextCache).length : 0;
217
+ let contextSizeKB = '0.0';
218
+ try {
219
+ const fs = await import('node:fs');
220
+ const path = await import('node:path');
221
+ const stat = fs.statSync(path.join(workspaceRoot, '.minovativemind', 'context_cache.json'));
222
+ contextSizeKB = (stat.size / 1024).toFixed(1);
223
+ }
224
+ catch (e) { }
225
+ console.log(`${pc.bold('Context Cache:')} ${pc.cyan(`${contextEntries} file summaries (${contextSizeKB} KB / 5 MB)`)}`);
226
+ }
227
+ catch (e) {
228
+ // Ignored if cache stats fail
229
+ }
166
230
  if (latestUsage) {
167
231
  if (latestUsage.remainingBalance !== undefined) {
168
232
  let diffStr = '';
@@ -336,6 +400,7 @@ export async function handleSlashCommand(command, context) {
336
400
  if (hasSessions) {
337
401
  options.push({ value: 'resume', label: 'View Chats' });
338
402
  options.push({ value: 'delete', label: 'Delete Chat' });
403
+ options.push({ value: 'bulk-delete', label: 'Bulk Delete Chats' });
339
404
  }
340
405
  options.push({ value: 'cancel', label: 'Cancel' });
341
406
  const truncate = (str, max) => {
@@ -550,6 +615,30 @@ export async function handleSlashCommand(command, context) {
550
615
  console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
551
616
  }
552
617
  }
618
+ else if (chatsMenu === 'bulk-delete') {
619
+ const selectedIds = await p['multiselect']({
620
+ message: 'Select chat sessions to delete:',
621
+ options: sessions.map((s) => ({
622
+ value: s.id,
623
+ label: `${truncate(s.title, 50)} (${new Date(s.timestamp).toLocaleString()})`,
624
+ })),
625
+ required: true,
626
+ });
627
+ if (p.isCancel(selectedIds) || !Array.isArray(selectedIds) || selectedIds.length === 0) {
628
+ p.log.warn('Bulk delete canceled.');
629
+ return { shouldContinue: true };
630
+ }
631
+ const confirm = await p['confirm']({
632
+ message: `Are you sure you want to delete ${selectedIds.length} session(s)?`,
633
+ });
634
+ if (confirm) {
635
+ await chatHistoryService.bulkDeleteSessions(selectedIds);
636
+ p.log.success(`Successfully deleted ${selectedIds.length} session(s).`);
637
+ }
638
+ else {
639
+ p.log.warn('Bulk delete canceled.');
640
+ }
641
+ }
553
642
  return { shouldContinue: true };
554
643
  }
555
644
  if (lowerCommand === '/workspaces') {
@@ -789,5 +878,66 @@ ${diffOut}
789
878
  }
790
879
  return { shouldContinue: true };
791
880
  }
881
+ if (lowerCommand === '/config-key') {
882
+ const creds = await loadCredentials();
883
+ const byokEnabled = !!(creds.useByok && creds.geminiApiKey);
884
+ const action = await p['select']({
885
+ message: `API Key Configuration (Current Mode: ${byokEnabled ? pc.green('BYOK') : pc.yellow('Credits')})`,
886
+ options: [
887
+ { value: 'toggle', label: byokEnabled ? 'Disable BYOK (Use Credits)' : 'Enable BYOK (Use API Key)' },
888
+ { value: 'set', label: 'Set/Update API Key' },
889
+ { value: 'clear', label: 'Clear API Key' },
890
+ { value: 'status', label: 'Check Status' },
891
+ { value: 'cancel', label: 'Cancel' },
892
+ ],
893
+ });
894
+ if (p.isCancel(action) || action === 'cancel')
895
+ return { shouldContinue: true };
896
+ if (action === 'toggle') {
897
+ if (!byokEnabled && !creds.geminiApiKey) {
898
+ p.log.error('You must set an API key before enabling BYOK mode.');
899
+ }
900
+ else {
901
+ await updateCredentialField('useByok', !byokEnabled);
902
+ p.log.success(`BYOK mode is now ${!byokEnabled ? pc.green('Enabled') : pc.yellow('Disabled')}`);
903
+ }
904
+ }
905
+ else if (action === 'set') {
906
+ const key = await p['password']({
907
+ message: 'Enter your Google AI Studio API Key:',
908
+ validate: (v) => (!v ? 'API key is required' : undefined),
909
+ });
910
+ if (p.isCancel(key))
911
+ return { shouldContinue: true };
912
+ const spinner = p.spinner();
913
+ spinner.start('Validating API Key...');
914
+ try {
915
+ // Test call to verify key
916
+ const client = new ProxyClient();
917
+ const idToken = (await getAuthorizedIdToken()) || '';
918
+ // We use a simple prompt to test the key
919
+ await client.generateFunctionCallViaProxy(idToken, 'gemini-3.5-flash-lite', [{ role: 'user', parts: [{ text: 'ping' }] }], undefined, undefined, 'You are a validator. Respond with "pong".', { maxOutputTokens: 10 });
920
+ // Reset metrics after validation
921
+ getAndResetTurnUsage();
922
+ await updateCredentialField('geminiApiKey', key);
923
+ spinner.stop('API Key validated and updated.');
924
+ p.log.success('API key saved.');
925
+ }
926
+ catch (err) {
927
+ spinner.stop('Validation failed.');
928
+ p.log.error(`Invalid API Key: ${err.message}`);
929
+ }
930
+ }
931
+ else if (action === 'clear') {
932
+ await updateCredentialField('geminiApiKey', undefined);
933
+ await updateCredentialField('useByok', false);
934
+ p.log.success('API key cleared and BYOK mode disabled.');
935
+ }
936
+ else if (action === 'status') {
937
+ p.log.info(`Mode: ${byokEnabled ? pc.green('BYOK') : pc.yellow('Credits')}\n` +
938
+ `Key Set: ${creds.geminiApiKey ? pc.green('Yes') : pc.red('No')}`);
939
+ }
940
+ return { shouldContinue: true };
941
+ }
792
942
  return { shouldContinue: true };
793
943
  }
@@ -115,8 +115,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
115
115
  const MAX_TURNS = Infinity;
116
116
  // Recovery thresholds for handling unexpected empty API payloads
117
117
  let emptyRetryCount = 0;
118
- const MAX_EMPTY_RETRIES = 3;
119
- const visitedToolCalls = new Set();
118
+ const MAX_EMPTY_RETRIES = 5;
120
119
  // Loop while the model keeps requesting tool calls
121
120
  while (true) {
122
121
  turnCount++;
@@ -179,7 +178,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
179
178
  const collector = getMetricCollector();
180
179
  if (collector)
181
180
  collector.recordToolTurn();
182
- const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal, visitedToolCalls);
181
+ const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal);
183
182
  if (execRes.aborted) {
184
183
  return '[Generation stopped by user]';
185
184
  }
@@ -222,7 +221,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
222
221
  response = followUp.response;
223
222
  }
224
223
  }
225
- async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal, visitedToolCalls) {
224
+ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal) {
226
225
  const toolResponses = [];
227
226
  for (const fc of functionCalls) {
228
227
  await inputHandler.waitForPrompt();
@@ -232,19 +231,6 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
232
231
  }
233
232
  const toolName = fc.name;
234
233
  const toolArgs = (fc.args ?? {});
235
- const callSignature = `${toolName}:${JSON.stringify(toolArgs)}`;
236
- if (visitedToolCalls.has(callSignature)) {
237
- toolResponses.push({
238
- functionResponse: {
239
- name: toolName,
240
- response: {
241
- error: 'You have already made this exact tool call previously. Please review your context history or try a different action.'
242
- }
243
- }
244
- });
245
- continue;
246
- }
247
- visitedToolCalls.add(callSignature);
248
234
  // Log the ongoing tool action to the console
249
235
  p.log.step(formatToolCall(toolName, toolArgs));
250
236
  // If executing a CLI/shell command, wait for approval
@@ -281,6 +267,8 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
281
267
  p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
282
268
  }
283
269
  }
270
+ if (!toolResult.error && (toolName === 'write_file' || toolName === 'modify_file' || toolName === 'delete_file')) {
271
+ }
284
272
  toolResponses.push({
285
273
  functionResponse: {
286
274
  name: toolName,
@@ -330,8 +330,20 @@ const DEFAULT_IGNORED_DIRS = new Set([
330
330
  '.cache',
331
331
  'coverage',
332
332
  '.turbo',
333
+ '.tmp',
334
+ 'temp',
335
+ 'tmp',
336
+ '.minovativemind',
337
+ ]);
338
+ const DEFAULT_IGNORED_FILES = new Set([
339
+ 'package-lock.json',
340
+ 'yarn.lock',
341
+ 'pnpm-lock.yaml',
342
+ '.DS_Store',
343
+ '.minovative-scratch.js',
344
+ 'testExtractor.ts',
345
+ 'testExtractor.js',
333
346
  ]);
334
- const DEFAULT_IGNORED_FILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.DS_Store']);
335
347
  /**
336
348
  * Parses .gitignore and .minovativemindignore to supplement the default ignore lists.
337
349
  */
@@ -339,7 +351,7 @@ async function getIgnoredPaths(workspaceRoot) {
339
351
  const ignoredDirs = new Set(DEFAULT_IGNORED_DIRS);
340
352
  const ignoredFiles = new Set(DEFAULT_IGNORED_FILES);
341
353
  const ig = ignore();
342
- ig.add(Array.from(DEFAULT_IGNORED_DIRS).map(d => d + '/'));
354
+ ig.add(Array.from(DEFAULT_IGNORED_DIRS).map((d) => d + '/'));
343
355
  ig.add(Array.from(DEFAULT_IGNORED_FILES));
344
356
  const filesToRead = ['.gitignore', '.minovativemindignore'];
345
357
  for (const ignoreFile of filesToRead) {
@@ -582,7 +594,7 @@ export async function writeFile(workspaceRoot, filePath, content) {
582
594
  if (!validation.valid) {
583
595
  return {
584
596
  output: '',
585
- error: `Syntax validation failed for "${filePath}":\\n- ${validation.errors.join('\\n- ')}\\n\\nPlease fix the syntax and try again.`,
597
+ error: `Syntax validation failed for "${filePath}":\n- ${validation.errors.join('\n- ')}\n\nPlease fix the syntax and try again.`,
586
598
  };
587
599
  }
588
600
  await fs.mkdir(path.dirname(absPath), { recursive: true });
@@ -594,7 +606,7 @@ export async function writeFile(workspaceRoot, filePath, content) {
594
606
  const message = err instanceof Error ? err.message : String(err);
595
607
  const collector = getMetricCollector();
596
608
  if (collector)
597
- collector.recordWriteFailure();
609
+ collector.recordWriteFailure?.();
598
610
  return {
599
611
  output: '',
600
612
  error: `Failed to write file "${filePath}": ${message}`,
@@ -645,7 +657,7 @@ export async function renameFile(workspaceRoot, sourcePath, targetPath) {
645
657
  }
646
658
  }
647
659
  export async function modifyFile(workspaceRoot, filePath, edits) {
648
- const MAX_MODIFY_RETRIES = 2;
660
+ const MAX_MODIFY_RETRIES = 4;
649
661
  const absPath = resolveAndValidatePath(workspaceRoot, filePath);
650
662
  for (let attempt = 1; attempt <= MAX_MODIFY_RETRIES; attempt++) {
651
663
  try {
@@ -658,17 +670,17 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
658
670
  if (!match) {
659
671
  const collector = getMetricCollector();
660
672
  if (collector)
661
- collector.recordModifyFailure();
673
+ collector.recordModifyFailure?.();
662
674
  if (attempt < MAX_MODIFY_RETRIES) {
663
675
  // Break out of inner loop, triggering a retry in outer loop
664
676
  modified = existing; // reset
665
677
  break;
666
678
  }
667
679
  // Provide a preview of the file to help the AI self-correct
668
- const preview = modified.split('\\n').slice(0, 20).join('\\n');
680
+ const preview = modified.split('\n').slice(0, 30).join('\n');
669
681
  return {
670
682
  output: '',
671
- error: `Edit #${i + 1} failed: Search content not found in "${filePath}".\\nFile start preview:\\n${preview}\\n...`,
683
+ error: `Edit #${i + 1} failed: Search content not found in "${filePath}". Please call 'read_file' to check the exact lines of code before retrying modify_file.\n\nFile start preview:\n${preview}\n...`,
672
684
  };
673
685
  }
674
686
  modified = applyMatch(modified, match, edit.replaceContent);
@@ -776,27 +788,27 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
776
788
  try {
777
789
  const { stdout, stderr } = await execAsync(command, {
778
790
  cwd: workspaceRoot,
779
- timeout: 60_000, // 60 second timeout
791
+ timeout: 120_000, // 120 second timeout
780
792
  maxBuffer: 1024 * 1024 * 2, // 2 MB buffer
781
793
  signal: abortSignal,
782
794
  });
783
795
  let output = [stdout, stderr].filter(Boolean).join('\n');
784
796
  // Truncate command output to prevent memory blowout from massive build logs
785
- const MAX_CMD_OUTPUT = 15_000;
797
+ const MAX_CMD_OUTPUT = 30_000;
786
798
  if (output.length > MAX_CMD_OUTPUT) {
787
799
  output =
788
800
  output.substring(0, MAX_CMD_OUTPUT) +
789
- `\n\n... (Output truncated: ${output.length} bytes exceeded 15KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
801
+ `\n\n... (Output truncated: ${output.length} bytes exceeded 30KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
790
802
  }
791
803
  return { output: output || '(command produced no output)' };
792
804
  }
793
805
  catch (err) {
794
806
  const message = err instanceof Error ? err.message : String(err);
795
807
  // Also truncate error output
796
- const MAX_ERR_OUTPUT = 15_000;
808
+ const MAX_ERR_OUTPUT = 30_000;
797
809
  const truncatedMsg = message.length > MAX_ERR_OUTPUT
798
810
  ? message.substring(0, MAX_ERR_OUTPUT) +
799
- `\n\n... (Error output truncated: ${message.length} bytes exceeded 15KB limit. Pipe to a file if you need full logs.)`
811
+ `\n\n... (Error output truncated: ${message.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
800
812
  : message;
801
813
  return { output: '', error: `Command failed: ${truncatedMsg}` };
802
814
  }
@@ -1281,7 +1293,7 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
1281
1293
  if (result.error) {
1282
1294
  const collector = getMetricCollector();
1283
1295
  if (collector)
1284
- collector.recordToolFailure(toolName);
1296
+ collector.recordToolFailure?.(toolName);
1285
1297
  }
1286
1298
  return result;
1287
1299
  }
@@ -86,7 +86,14 @@ export async function startAgentLoop(workspaceRoot, version) {
86
86
  chatHistoryService.init(workspaceRoot);
87
87
  const chat = createSharedChatSession();
88
88
  const inputHandler = new AsyncInputHandler();
89
- const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, modelUsageCounts: {} };
89
+ const chatSessionState = {
90
+ id: crypto.randomUUID(),
91
+ title: '',
92
+ totalTokens: 0,
93
+ totalInputTokens: 0,
94
+ totalOutputTokens: 0,
95
+ modelUsageCounts: {},
96
+ };
90
97
  chat.setSessionInfo(chatSessionState.id, workspaceRoot);
91
98
  const sessionInputHistory = [];
92
99
  let isRawPasteMode = false;
@@ -137,11 +144,23 @@ export async function startAgentLoop(workspaceRoot, version) {
137
144
  message: 'Command Menu',
138
145
  options: [
139
146
  { value: '/models', label: '/models', hint: 'Change the active AI model' },
140
- { value: '/plan', label: '/plan', hint: `Toggle plan mode (build a plan without executing) ${isPlanMode ? pc.green('(ON)') : pc.red('(OFF)')}` },
147
+ {
148
+ value: '/plan',
149
+ label: '/plan',
150
+ hint: `Toggle plan mode (build a plan without executing) ${isPlanMode ? pc.green('(ON)') : pc.red('(OFF)')}`,
151
+ },
141
152
  { value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
142
153
  { value: '/clear', label: '/clear', hint: 'Clear chat session history' },
143
- { value: '/debug', label: '/debug', hint: `Toggle internal debug logs ${isDebugOn() ? pc.green('(ON)') : pc.red('(OFF)')}` },
144
- { value: '/auto-approve', label: '/auto-approve', hint: `Approve all future terminal commands ${getApprovalMode() === 'skip-all' ? pc.green('(ON)') : pc.red('(OFF)')}` },
154
+ {
155
+ value: '/debug',
156
+ label: '/debug',
157
+ hint: `Toggle internal debug logs ${isDebugOn() ? pc.green('(ON)') : pc.red('(OFF)')}`,
158
+ },
159
+ {
160
+ value: '/auto-approve',
161
+ label: '/auto-approve',
162
+ hint: `Approve all future terminal commands ${getApprovalMode() === 'skip-all' ? pc.green('(ON)') : pc.red('(OFF)')}`,
163
+ },
145
164
  {
146
165
  value: '/sub-agents',
147
166
  label: '/sub-agents',
@@ -152,6 +171,7 @@ export async function startAgentLoop(workspaceRoot, version) {
152
171
  label: '/workspaces',
153
172
  hint: 'Manage external workspaces for cross-project development',
154
173
  },
174
+ { value: '/config-key', label: '/config-key', hint: 'BYOK (Bring Your Own Key) Configuration' },
155
175
  { value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
156
176
  { value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
157
177
  { value: '/revert', label: '/revert', hint: 'Undo last change' },
@@ -235,6 +255,8 @@ export async function startAgentLoop(workspaceRoot, version) {
235
255
  }
236
256
  export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult) {
237
257
  return runWithAgentId('main', async () => {
258
+ const { resetTurnAccumulator } = await import('./metrics.js');
259
+ resetTurnAccumulator();
238
260
  const turnStartTime = Date.now();
239
261
  const spinner = p.spinner();
240
262
  const ac = new AbortController();
@@ -505,8 +527,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
505
527
  const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
506
528
  p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
507
529
  }
508
- const inputTokens = (usage.promptTokens || 0) + (usage.cachedTokens || 0);
509
- const outputTokens = usage.candidatesTokens || 0;
530
+ const { getTurnTotals } = await import('./metrics.js');
531
+ const totals = getTurnTotals();
532
+ if (gatherRes && gatherRes.contextResult && gatherRes.contextResult.fromMemoryBank) {
533
+ p.log.info(`${pc.green('⚡')} ${pc.green('Memory Bank:')} Investigation skipped ${pc.dim(`(saved ~5 context agent turns)`)}`);
534
+ }
535
+ const inputTokens = (totals.promptTokens || usage.promptTokens || 0) + (totals.cachedTokens || usage.cachedTokens || 0);
536
+ const outputTokens = totals.outputTokens || usage.candidatesTokens || 0;
510
537
  const totalTokens = inputTokens + outputTokens;
511
538
  p.log.info(`${pc.dim('Tokens Used:')} ${pc.cyan(totalTokens.toLocaleString())} ${pc.dim(`(Input: ${inputTokens.toLocaleString()}, Output: ${outputTokens.toLocaleString()})`)}`);
512
539
  if (usage.remainingBalance !== undefined) {
@@ -740,13 +767,16 @@ async function compressContextFiles(workspaceRoot, contextResult) {
740
767
  }
741
768
  }
742
769
  if (cacheUpdated) {
743
- // Keep cache size manageable by restricting to recent 500 file entries
744
- const keys = Object.keys(cachedContext);
745
- if (keys.length > 500) {
746
- const toDelete = keys.length - 500;
747
- for (let i = 0; i < toDelete; i++) {
748
- delete cachedContext[keys[i]];
749
- }
770
+ // Keep cache size manageable by restricting to 5MB total size
771
+ const MAX_CACHE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
772
+ let cacheJson = JSON.stringify(cachedContext);
773
+ while (Buffer.byteLength(cacheJson, 'utf8') > MAX_CACHE_SIZE_BYTES) {
774
+ const keys = Object.keys(cachedContext);
775
+ if (keys.length === 0)
776
+ break;
777
+ // Remove oldest entries (assuming keys are inserted chronologically)
778
+ delete cachedContext[keys[0]];
779
+ cacheJson = JSON.stringify(cachedContext);
750
780
  }
751
781
  writeCache(workspaceRoot, 'context_cache.json', cachedContext);
752
782
  }
@@ -1,4 +1,4 @@
1
- import type { Content, FunctionCall } from '@google/generative-ai';
1
+ import { type Content, type FunctionCall } from '@google/generative-ai';
2
2
  export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
3
3
  export declare function clearModelOverrides(): void;
4
4
  export declare function setGlobalActiveModel(model: string): void;
@@ -76,7 +76,7 @@ export declare function getPlanModeConfig(): {
76
76
  * Compresses a large string of text using gemini-3.5-flash-lite.
77
77
  * Used for shrinking context payloads to prevent OOM/choking.
78
78
  */
79
- export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any): Promise<string>;
79
+ export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any, force?: boolean): Promise<string>;
80
80
  /**
81
81
  * Returns the tool declarations for the read-only Context Agent.
82
82
  * Extracted as a reusable function so investigation sub-agents can import