minovative-mind-cli 1.5.1 → 2.0.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 (75) hide show
  1. package/README.md +51 -45
  2. package/dist/commands/chat.js +7 -2
  3. package/dist/services/agent/slashCommands.js +156 -30
  4. package/dist/services/agent/toolLoop.d.ts +1 -1
  5. package/dist/services/agent/toolLoop.js +7 -2
  6. package/dist/services/agent/types.d.ts +2 -0
  7. package/dist/services/agent-tools.d.ts +9 -4
  8. package/dist/services/agent-tools.js +145 -21
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +285 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +167 -35
  13. package/dist/services/changeLogger.d.ts +142 -0
  14. package/dist/services/changeLogger.js +132 -3
  15. package/dist/services/contextAgent.d.ts +6 -1
  16. package/dist/services/contextAgent.js +95 -14
  17. package/dist/services/embeddingIndex.d.ts +82 -0
  18. package/dist/services/embeddingIndex.js +613 -0
  19. package/dist/services/investigationComplexity.d.ts +45 -0
  20. package/dist/services/investigationComplexity.js +91 -0
  21. package/dist/services/metrics.d.ts +18 -0
  22. package/dist/services/metrics.js +7 -0
  23. package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
  24. package/dist/services/orchestration/fileLockRegistry.js +276 -0
  25. package/dist/services/orchestration/investigationAgent.d.ts +85 -0
  26. package/dist/services/orchestration/investigationAgent.js +359 -0
  27. package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
  28. package/dist/services/orchestration/investigationOrchestrator.js +180 -0
  29. package/dist/services/orchestration/messageBus.d.ts +162 -0
  30. package/dist/services/orchestration/messageBus.js +225 -0
  31. package/dist/services/orchestration/orchestrator.d.ts +45 -0
  32. package/dist/services/orchestration/orchestrator.js +214 -0
  33. package/dist/services/orchestration/readCache.d.ts +79 -0
  34. package/dist/services/orchestration/readCache.js +108 -0
  35. package/dist/services/orchestration/scopedTools.d.ts +57 -0
  36. package/dist/services/orchestration/scopedTools.js +172 -0
  37. package/dist/services/orchestration/subAgent.d.ts +58 -0
  38. package/dist/services/orchestration/subAgent.js +187 -0
  39. package/dist/services/orchestration/taskGraph.d.ts +129 -0
  40. package/dist/services/orchestration/taskGraph.js +254 -0
  41. package/dist/services/proxyClient.d.ts +25 -0
  42. package/dist/services/proxyClient.js +60 -0
  43. package/dist/utils/asyncContext.d.ts +16 -0
  44. package/dist/utils/asyncContext.js +25 -0
  45. package/dist/utils/config.d.ts +3 -1
  46. package/dist/utils/config.js +3 -1
  47. package/dist/utils/contextPrompts.js +3 -2
  48. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  49. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  50. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  52. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  53. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  54. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  56. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  57. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  58. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  59. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  60. package/dist/utils/dependencyTracer.js +31 -17
  61. package/dist/utils/excludedExtensions.js +0 -1
  62. package/dist/utils/historyPrompt.d.ts +9 -0
  63. package/dist/utils/historyPrompt.js +87 -0
  64. package/dist/utils/logo.js +7 -7
  65. package/dist/utils/paste.d.ts +21 -0
  66. package/dist/utils/paste.js +22 -1
  67. package/dist/utils/profiles.d.ts +2 -0
  68. package/dist/utils/profiles.js +44 -0
  69. package/dist/utils/projectStorage.js +10 -7
  70. package/dist/utils/systemPrompts.d.ts +6 -3
  71. package/dist/utils/systemPrompts.js +106 -5
  72. package/dist/utils/types.d.ts +33 -0
  73. package/dist/utils/types.js +1 -0
  74. package/oclif.manifest.json +2 -2
  75. package/package.json +4 -3
@@ -23,17 +23,23 @@ import { marked } from 'marked';
23
23
  import { markedTerminal } from 'marked-terminal';
24
24
  import { debugLog, isDebugOn } from '../utils/logger.js';
25
25
  import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents, } from '../utils/projectStorage.js';
26
- import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, } from './ai.js';
26
+ import { GEMINI_MODELS } from '../utils/config.js';
27
+ import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, getGlobalLatestUsageMetadata, } from './ai.js';
27
28
  import { changeLogger } from './changeLogger.js';
28
29
  import { chatHistoryService } from './chatHistoryService.js';
29
- import { gatherContext, routeIntent } from './contextAgent.js';
30
+ import { gatherContext, routeIntent, evaluateExecutionComplexity } from './contextAgent.js';
30
31
  import { verifyChangedFiles } from './verificationService.js';
31
32
  import { buildContextInjection } from '../utils/contextPrompts.js';
33
+ import { historyText } from '../utils/historyPrompt.js';
32
34
  // Submodule Imports
33
35
  import { AsyncInputHandler } from './agent/inputHandler.js';
34
36
  import { processResponse } from './agent/toolLoop.js';
35
37
  import { handleSlashCommand } from './agent/slashCommands.js';
36
- marked.use(markedTerminal());
38
+ import { getMetricCollector } from './metrics.js';
39
+ import { Orchestrator } from './orchestration/orchestrator.js';
40
+ import { isSubAgentsEnabled } from './agent-tools.js';
41
+ import { runWithAgentId } from '../utils/asyncContext.js';
42
+ marked.use(markedTerminal({ reflowText: false }));
37
43
  // Export submodules for potential external uses if required
38
44
  export { AsyncInputHandler } from './agent/inputHandler.js';
39
45
  /**
@@ -77,7 +83,9 @@ export async function startAgentLoop(workspaceRoot, version) {
77
83
  const chat = createSharedChatSession();
78
84
  const inputHandler = new AsyncInputHandler();
79
85
  const chatSessionState = { id: crypto.randomUUID(), title: '' };
86
+ const sessionInputHistory = [];
80
87
  let isRawPasteMode = false;
88
+ let isPlanMode = false;
81
89
  // Hook process.stdin.emit to intercept fast stream inputs.
82
90
  // When large buffers containing newlines arrive rapidly, we interpret them as a clipboard paste,
83
91
  // sanitizing and collapsing them to prevent premature command line submission.
@@ -102,7 +110,7 @@ export async function startAgentLoop(workspaceRoot, version) {
102
110
  }
103
111
  inputHandler.stop();
104
112
  // Input collection (delegated to a helper function to avoid nested loop warning)
105
- const { userInput: rawInput, canceled } = await collectUserInput();
113
+ const { userInput: rawInput, canceled } = await collectUserInput(sessionInputHistory, isPlanMode);
106
114
  if (canceled) {
107
115
  p.outro(pc.green('Goodbye! Happy coding. 🚀'));
108
116
  return;
@@ -111,16 +119,28 @@ export async function startAgentLoop(workspaceRoot, version) {
111
119
  if (!userInput) {
112
120
  continue;
113
121
  }
122
+ // Push non-empty input to history if it's not the same as the last item
123
+ if (sessionInputHistory[sessionInputHistory.length - 1] !== userInput) {
124
+ sessionInputHistory.push(userInput);
125
+ }
114
126
  // Capture standalone forward slash triggers to open the selection console
115
127
  if (userInput === '/') {
116
128
  const commandMenu = await p['select']({
117
129
  message: 'Command Menu',
118
130
  options: [
119
131
  { value: '/models', label: '/models', hint: 'Change the active AI model' },
132
+ { value: '/plan', label: '/plan', hint: 'Toggle plan mode (build a plan without executing)' },
120
133
  { value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
121
134
  { value: '/clear', label: '/clear', hint: 'Clear chat session history' },
122
135
  { value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
123
136
  { value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
137
+ { value: '/sub-agents', label: '/sub-agents', hint: 'Toggle the MMAAK Engine for parallel investigation and execution' },
138
+ {
139
+ value: '/semantic-search',
140
+ label: '/semantic-search',
141
+ hint: 'Toggle local vector index capabilities for better search',
142
+ },
143
+ { value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
124
144
  { value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
125
145
  { value: '/revert', label: '/revert', hint: 'Undo last change' },
126
146
  { value: '/chats', label: '/chats', hint: 'View or resume past chat sessions' },
@@ -151,12 +171,16 @@ export async function startAgentLoop(workspaceRoot, version) {
151
171
  workspaceRoot,
152
172
  version,
153
173
  isRawPasteMode,
174
+ isPlanMode,
154
175
  chatSessionState,
155
176
  };
156
177
  const slashResult = await handleSlashCommand(userInput, slashCtx);
157
178
  if (slashResult.isRawPasteMode !== undefined) {
158
179
  isRawPasteMode = slashResult.isRawPasteMode;
159
180
  }
181
+ if (slashResult.isPlanModeOverride !== undefined) {
182
+ isPlanMode = slashResult.isPlanModeOverride;
183
+ }
160
184
  if (slashResult.userInputOverride !== undefined) {
161
185
  userInput = slashResult.userInputOverride;
162
186
  }
@@ -165,6 +189,36 @@ export async function startAgentLoop(workspaceRoot, version) {
165
189
  }
166
190
  }
167
191
  // ─── Main Execution Cycle ────────────────────────────────────────
192
+ let currentInput = userInput;
193
+ let cachedContextResult = null;
194
+ while (true) {
195
+ const execResult = await executeSingleTurn(workspaceRoot, currentInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult);
196
+ const planModeReturn = execResult?.planModeReturn;
197
+ const contextResult = execResult?.contextResult;
198
+ if (isPlanMode && planModeReturn) {
199
+ if (planModeReturn === 'proceed') {
200
+ isPlanMode = false;
201
+ currentInput = 'Please execute the plan we just finalized.';
202
+ cachedContextResult = contextResult;
203
+ p.log.info(pc.cyan('Proceeding with execution...'));
204
+ continue;
205
+ }
206
+ else if (planModeReturn === 'edit') {
207
+ p.log.info(pc.cyan('Please provide additional instructions to revise the plan.'));
208
+ break;
209
+ }
210
+ else if (planModeReturn === 'exit') {
211
+ isPlanMode = false;
212
+ p.log.info(pc.cyan('Exited plan mode. Returning to normal chat.'));
213
+ break;
214
+ }
215
+ }
216
+ break;
217
+ }
218
+ }
219
+ }
220
+ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult) {
221
+ return runWithAgentId('main', async () => {
168
222
  const turnStartTime = Date.now();
169
223
  const spinner = p.spinner();
170
224
  const ac = new AbortController();
@@ -175,12 +229,20 @@ export async function startAgentLoop(workspaceRoot, version) {
175
229
  let finalInput = userInput;
176
230
  // Stage 1: Gather Workspace Context and route intentions
177
231
  spinner.start('🔍 Investigating workspace...');
178
- const chatHistory = chat.getRecentHistory(3);
179
- const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
180
- if (!inputHandler.isCurrentlyPrompting()) {
181
- spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
182
- }
183
- });
232
+ let gatherRes = { targetAgent: 'EXECUTE', chainedMessages: [], contextResult: cachedContextResult || null };
233
+ if (!cachedContextResult) {
234
+ const collector = getMetricCollector();
235
+ if (collector)
236
+ collector.startTimer('contextGather');
237
+ const chatHistory = chat.getRecentHistory(3);
238
+ gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
239
+ if (!inputHandler.isCurrentlyPrompting()) {
240
+ spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
241
+ }
242
+ });
243
+ if (collector)
244
+ collector.stopTimer('contextGather');
245
+ }
184
246
  let latestUsage = undefined;
185
247
  // Collect any inputs that were queued while the Context Agent was investigating
186
248
  const leftoverMsg = inputHandler.getAndClear();
@@ -213,26 +275,64 @@ export async function startAgentLoop(workspaceRoot, version) {
213
275
  }
214
276
  // Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
215
277
  debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
216
- const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
278
+ let config;
279
+ if (isPlanMode) {
280
+ const { getPlanModeConfig } = await import('./ai.js');
281
+ config = getPlanModeConfig();
282
+ }
283
+ else {
284
+ config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
285
+ }
217
286
  let dynamicSystemInstruction = config.systemInstruction;
218
287
  // Inject gathered workspace directories, dependency configurations, and matching search patterns
219
288
  if (gatherRes.contextResult) {
220
289
  // Compress each relevant file individually using helper to avoid nested loop warning
221
- gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
290
+ if (!cachedContextResult) {
291
+ gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
292
+ }
222
293
  // Assemble the final context injection string
223
294
  const contextInjection = buildContextInjection(gatherRes.contextResult);
224
295
  debugLog(`Final compressed context injection size: ${contextInjection.length} chars`);
296
+ const collector = getMetricCollector();
297
+ if (collector)
298
+ collector.recordCompressedContextSize(contextInjection.length);
225
299
  dynamicSystemInstruction += '\n\n' + contextInjection;
226
300
  }
227
301
  // Apply the dynamic prompt updates and tool registrations to the active chat session
228
302
  chat.setAgentConfig(dynamicSystemInstruction, config.tools);
303
+ if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
304
+ let selectedModel = GEMINI_MODELS.FLASH_3_5;
305
+ if (effectiveTargetAgent === 'CHAT') {
306
+ selectedModel = GEMINI_MODELS.FLASH_LITE_3_1;
307
+ }
308
+ else {
309
+ spinner.start(pc.blue('🧠 Evaluating execution complexity...'));
310
+ const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
311
+ spinner.stop();
312
+ process.stdout.write('\x1b[2K\r');
313
+ selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE_3_1 : GEMINI_MODELS.FLASH_3_5;
314
+ }
315
+ chat.setModel(selectedModel);
316
+ debugLog(`Auto-routing to model ${selectedModel} based on intent ${effectiveTargetAgent}`);
317
+ }
318
+ else {
319
+ chat.setModel(getGlobalActiveModel());
320
+ }
229
321
  if (gatherRes.contextResult) {
230
- if (!inputHandler.isCurrentlyPrompting()) {
231
- spinner.stop(pc.green('🔍 Investigation completed.'));
232
- spinner.start('Thinking...');
322
+ if (!gatherRes.contextResult.isParallel) {
323
+ if (!inputHandler.isCurrentlyPrompting()) {
324
+ spinner.stop(pc.green('🔍 Investigation completed.'));
325
+ spinner.start('Thinking...');
326
+ }
327
+ else {
328
+ p.log.success(pc.green('🔍 Investigation completed.'));
329
+ }
233
330
  }
234
331
  else {
235
- p.log.success(pc.green('🔍 Investigation completed.'));
332
+ // Parallel investigation already printed its own detailed summary log
333
+ if (!inputHandler.isCurrentlyPrompting()) {
334
+ spinner.start('Thinking...');
335
+ }
236
336
  }
237
337
  }
238
338
  else {
@@ -240,27 +340,106 @@ export async function startAgentLoop(workspaceRoot, version) {
240
340
  spinner.message('Thinking...');
241
341
  }
242
342
  }
343
+ if (effectiveTargetAgent === 'EXECUTE' && isSubAgentsEnabled()) {
344
+ spinner.stop(); // Clear the spinner before delegating
345
+ process.stdout.write('\x1b[2K\r');
346
+ const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
347
+ const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
348
+ if (typeof handledByOrchestrator === 'string') {
349
+ // The orchestrator successfully decomposed and ran the task.
350
+ // Inject the task context into the main proxy chat session so the agent remembers what happened
351
+ // and so the history state gets saved properly on disk.
352
+ chat.getRawHistory().push({
353
+ role: 'user',
354
+ parts: [{ text: userInput }],
355
+ });
356
+ chat.getRawHistory().push({
357
+ role: 'model',
358
+ parts: [{ text: handledByOrchestrator }],
359
+ });
360
+ // Print the summary text just like single-agent mode
361
+ if (handledByOrchestrator !== '[Generation stopped.]') {
362
+ console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
363
+ const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
364
+ console.log(marked.parse(cleanText));
365
+ }
366
+ // Print Usage Stats
367
+ const usage = getGlobalLatestUsageMetadata();
368
+ if (usage) {
369
+ if (usage.cachedTokens > 0) {
370
+ const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
371
+ const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
372
+ p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
373
+ }
374
+ if (usage.remainingBalance !== undefined) {
375
+ p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
376
+ }
377
+ }
378
+ // Post-execution steps for the UI (saving history, invalidate cache)
379
+ const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
380
+ const modifiedFiles = postExecutionChanges
381
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
382
+ .map((c) => path.relative(workspaceRoot, c.filePath));
383
+ if (modifiedFiles.length > 0) {
384
+ await invalidateCacheForDependents(workspaceRoot, modifiedFiles);
385
+ }
386
+ const turnEndTime = Date.now();
387
+ const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
388
+ p.log.info(`${pc.dim('Orchestration finished in')} ${pc.cyan(turnDuration + 's')}`);
389
+ changeLogger.markComplete();
390
+ changeLogger.commitChangeSet();
391
+ // Auto-save chat history for the orchestrator turn
392
+ const history = chat.getRawHistory();
393
+ if (history.length > 0) {
394
+ if (!chatSessionState.title) {
395
+ chatSessionState.title = 'Generating title...';
396
+ try {
397
+ const title = await generateChatTitle(userInput);
398
+ chatSessionState.title = title;
399
+ }
400
+ catch (e) {
401
+ debugLog('Failed to save session with title: ' + e);
402
+ }
403
+ }
404
+ chatHistoryService
405
+ .saveSession({
406
+ id: chatSessionState.id,
407
+ title: chatSessionState.title,
408
+ timestamp: Date.now(),
409
+ history,
410
+ })
411
+ .catch((e) => debugLog('Failed to auto-save session: ' + e));
412
+ }
413
+ return;
414
+ }
415
+ // If it returned false, the graph was too simple (1 task) or failed, so we fall through to the single-agent loop.
416
+ if (!inputHandler.isCurrentlyPrompting()) {
417
+ spinner.start('Thinking (Single-Agent)...');
418
+ }
419
+ }
243
420
  // Send the formulated prompt payload to the generative model
244
421
  let result;
245
422
  try {
246
423
  result = await chat.sendMessage(finalInput, undefined, ac.signal);
247
424
  }
248
425
  catch (e) {
249
- spinner.stop('');
426
+ spinner.stop();
427
+ process.stdout.write('\x1b[2K\r');
250
428
  if (e.name === 'AbortError' || e.message?.includes('abort')) {
251
429
  p.log.warn(pc.yellow('Generation aborted by user.'));
252
430
  }
253
431
  else {
254
432
  p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
255
433
  }
256
- continue;
434
+ return;
257
435
  }
258
436
  latestUsage = result.response.usageMetadata?.();
259
437
  const grounding = result.response.groundingMetadata?.();
260
438
  if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
261
439
  p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
262
440
  }
263
- spinner.stop('');
441
+ spinner.stop();
442
+ process.stdout.write('\x1b[2K\r');
264
443
  const calls = result.response.functionCalls();
265
444
  debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
266
445
  // Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
@@ -303,44 +482,95 @@ export async function startAgentLoop(workspaceRoot, version) {
303
482
  if (history.length > 0) {
304
483
  if (!chatSessionState.title) {
305
484
  chatSessionState.title = 'Generating title...';
306
- generateChatTitle(userInput).then(title => {
485
+ try {
486
+ const title = await generateChatTitle(userInput);
307
487
  chatSessionState.title = title;
308
- chatHistoryService.saveSession({
488
+ await chatHistoryService.saveSession({
309
489
  id: chatSessionState.id,
310
490
  title: chatSessionState.title,
311
491
  timestamp: Date.now(),
312
- history: chat.getRawHistory()
313
- }).catch(e => debugLog('Failed to save session with title: ' + e));
314
- });
492
+ history: chat.getRawHistory(),
493
+ });
494
+ }
495
+ catch (e) {
496
+ debugLog('Failed to save session with title: ' + e);
497
+ }
315
498
  }
316
- chatHistoryService.saveSession({
499
+ chatHistoryService
500
+ .saveSession({
317
501
  id: chatSessionState.id,
318
502
  title: chatSessionState.title,
319
503
  timestamp: Date.now(),
320
- history
321
- }).catch(e => debugLog('Failed to auto-save session: ' + e));
504
+ history,
505
+ })
506
+ .catch((e) => debugLog('Failed to auto-save session: ' + e));
507
+ }
508
+ let planModeReturn = undefined;
509
+ if (isPlanMode && finalText && finalText !== '[Generation stopped.]') {
510
+ inputHandler.stop(); // MUST stop the async input handler before showing an interactive prompt, or it will intercept stdin and freeze/duplicate
511
+ // Ensure the terminal has fully scrolled and there is a clean line break
512
+ console.log('\n');
513
+ // Small delay to allow stdout to flush and terminal to finish scrolling from the markdown output
514
+ // This prevents @clack/prompts from miscalculating the cursor position and duplicating the prompt on arrow keys
515
+ await new Promise((resolve) => setTimeout(resolve, 200));
516
+ const planAction = await p['select']({
517
+ message: 'Plan generated. What would you like to do?',
518
+ options: [
519
+ { value: 'proceed', label: 'Proceed with plan', hint: 'Executes the generated plan immediately' },
520
+ { value: 'edit', label: 'Edit plan', hint: 'Provide additional feedback to revise the plan' },
521
+ { value: 'exit', label: 'Exit plan mode', hint: 'Return to normal chat without executing' },
522
+ ],
523
+ });
524
+ if (p.isCancel(planAction) || planAction === 'exit') {
525
+ planModeReturn = 'exit';
526
+ }
527
+ else if (planAction === 'edit') {
528
+ planModeReturn = 'edit';
529
+ }
530
+ else if (planAction === 'proceed') {
531
+ planModeReturn = 'proceed';
532
+ }
322
533
  }
534
+ return { planModeReturn, contextResult: gatherRes.contextResult };
323
535
  }
324
536
  catch (err) {
325
- spinner.stop('');
326
537
  const message = err instanceof Error ? err.message : String(err);
327
538
  p.log.error(`${pc.red('Error:')} ${message}`);
328
539
  }
329
540
  finally {
541
+ spinner.stop();
542
+ process.stdout.write('\x1b[2K\r');
330
543
  // Persist any verified or partial changes into our session change ledger.
331
544
  // This ensures that if the process crashes, errors out, or is aborted mid-generation,
332
545
  // the user can still use /revert to undo the partial file mutations.
546
+ const changedFiles = changeLogger.getChangedFiles();
333
547
  changeLogger.commitChangeSet();
548
+ // Delta-update the embedding index if there were changes
549
+ if (changedFiles.length > 0) {
550
+ try {
551
+ const { getEmbeddingIndex } = await import('./embeddingIndex.js');
552
+ const index = getEmbeddingIndex();
553
+ // Only update if it's already loaded in memory
554
+ if (index.isReady()) {
555
+ await index.updateIndex(workspaceRoot, changedFiles);
556
+ await index.save(workspaceRoot);
557
+ }
558
+ }
559
+ catch (e) {
560
+ debugLog(`Failed to delta-update embedding index: ${e.message}`);
561
+ }
562
+ }
334
563
  }
335
- }
564
+ });
336
565
  }
337
- async function collectUserInput() {
566
+ async function collectUserInput(history, isPlanMode) {
338
567
  let lines = [];
339
568
  let isMultiLine = false;
340
569
  while (true) {
341
- const userInputRaw = await p.text({
342
- message: isMultiLine ? ' ' : pc.magenta('❯'),
570
+ const userInputRaw = await historyText({
571
+ message: isMultiLine ? ' ' : isPlanMode ? pc.cyan('(Plan Mode) ❯') : pc.magenta('❯'),
343
572
  placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
573
+ history,
344
574
  });
345
575
  if (p.isCancel(userInputRaw)) {
346
576
  if (isMultiLine) {
@@ -376,10 +606,12 @@ async function compressContextFiles(workspaceRoot, contextResult) {
376
606
  const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
377
607
  let cacheUpdated = false;
378
608
  const compressedFiles = new Map();
379
- for (const [filePath, content] of contextResult.relevantFiles.entries()) {
380
- if (content.length < 2000) {
609
+ for (const [filePath, contentObj] of contextResult.relevantFiles.entries()) {
610
+ const content = contentObj.text;
611
+ const inlineData = contentObj.inlineData;
612
+ if (content.length < 2000 && !inlineData) {
381
613
  debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
382
- compressedFiles.set(filePath, content);
614
+ compressedFiles.set(filePath, contentObj);
383
615
  continue;
384
616
  }
385
617
  const fileHash = crypto
@@ -388,13 +620,13 @@ async function compressContextFiles(workspaceRoot, contextResult) {
388
620
  .digest('hex');
389
621
  if (cachedContext[fileHash]) {
390
622
  debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
391
- compressedFiles.set(filePath, cachedContext[fileHash]);
623
+ compressedFiles.set(filePath, { text: cachedContext[fileHash] });
392
624
  }
393
625
  else {
394
626
  debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
395
627
  const compressPrompt = `Summarize the following file contents concisely. Preserve all exports, functions, classes, variables, and architectural purpose. Keep it under 2000 characters if possible. File: ${filePath}`;
396
- const summary = await compressTextUsingFlashLite(content, compressPrompt);
397
- compressedFiles.set(filePath, summary);
628
+ const summary = await compressTextUsingFlashLite(content, compressPrompt, inlineData);
629
+ compressedFiles.set(filePath, { text: summary });
398
630
  cachedContext[fileHash] = summary;
399
631
  cacheUpdated = true;
400
632
  }
@@ -441,8 +673,12 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
441
673
  const forcePrompt = `AUTOMATED SYSTEM CHECK: You did not modify any files. You MUST use your file modification tools to apply a fix for the previously mentioned errors. Do not just explain the issue.`;
442
674
  spinner.start('Thinking (Correction)...');
443
675
  result = await chat.sendMessage(forcePrompt);
444
- spinner.stop('');
676
+ spinner.stop();
677
+ process.stdout.write('\x1b[2K\r');
445
678
  correctionAttempts++;
679
+ const collector = getMetricCollector();
680
+ if (collector)
681
+ collector.recordSelfCorrection();
446
682
  continue;
447
683
  }
448
684
  previousChangeCount = currentChanges.length;
@@ -466,9 +702,17 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
466
702
  }
467
703
  if (!hasErrors) {
468
704
  p.log.success('Verification passed.');
705
+ const collector = getMetricCollector();
706
+ if (collector && correctionAttempts === 0)
707
+ collector.recordVerificationResult(true);
469
708
  break;
470
709
  }
710
+ const collector = getMetricCollector();
711
+ if (collector && correctionAttempts === 0)
712
+ collector.recordVerificationResult(false);
471
713
  correctionAttempts++;
714
+ if (collector)
715
+ collector.recordSelfCorrection();
472
716
  if (correctionAttempts > MAX_CORRECTIONS) {
473
717
  p.log.warn('Max self-correction attempts reached. Leaving remaining issues for manual review.');
474
718
  break;
@@ -496,7 +740,8 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
496
740
  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.`;
497
741
  spinner.start('Thinking (Correction)...');
498
742
  result = await chat.sendMessage(correctionPrompt);
499
- spinner.stop('');
743
+ spinner.stop();
744
+ process.stdout.write('\x1b[2K\r');
500
745
  }
501
746
  return { finalText };
502
747
  }
@@ -1,4 +1,9 @@
1
1
  import type { Content, FunctionCall } from '@google/generative-ai';
2
+ export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
3
+ export declare function clearModelOverrides(): void;
4
+ export declare function setGlobalActiveModel(model: string): void;
5
+ export declare function getGlobalActiveModel(): string;
6
+ export declare function getGlobalLatestUsageMetadata(): any;
2
7
  export declare class ProxyChatSession {
3
8
  private history;
4
9
  private modelName;
@@ -31,7 +36,7 @@ export declare class ProxyChatSession {
31
36
  name: string;
32
37
  response: any;
33
38
  };
34
- }>, additionalText?: string, abortSignal?: AbortSignal): Promise<{
39
+ }>, additionalText?: string, abortSignal?: AbortSignal, onChunk?: (chunk: string) => void): Promise<{
35
40
  response: {
36
41
  text: () => string;
37
42
  functionCalls: () => FunctionCall[] | undefined;
@@ -56,16 +61,25 @@ export declare function getPlanExecutionConfig(): {
56
61
  functionDeclarations: import("@google/generative-ai").FunctionDeclaration[];
57
62
  }[];
58
63
  };
64
+ export declare function getPlanModeConfig(): {
65
+ systemInstruction: string;
66
+ tools: never[];
67
+ };
59
68
  /**
60
69
  * Compresses a large string of text using gemini-3.1-flash-lite.
61
70
  * Used for shrinking context payloads to prevent OOM/choking.
62
71
  */
63
- export declare function compressTextUsingFlashLite(text: string, instruction?: string): Promise<string>;
64
- export declare const CONTEXT_AGENT_MODEL: "gemini-3.5-flash";
72
+ export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any): Promise<string>;
73
+ /**
74
+ * Returns the tool declarations for the read-only Context Agent.
75
+ * Extracted as a reusable function so investigation sub-agents can import
76
+ * the same schema without duplicating the declarations.
77
+ */
78
+ export declare function getContextToolDeclarations(): any[];
65
79
  export declare function createContextAgentSession(): any;
66
- export declare const INTENT_ROUTER_MODEL: "gemini-3.1-flash-lite";
67
80
  export declare function createIntentRouterSession(): any;
68
- export declare const WEB_SEARCH_AGENT_MODEL: "gemini-3.1-flash-lite";
81
+ export declare function createExecutionComplexitySession(): any;
82
+ export declare function createInvestigationComplexitySession(): any;
69
83
  export declare function createWebSearchAgentSession(): any;
70
84
  /**
71
85
  * Generates a concise title for a chat session based on the user's first message.