minovative-mind-cli 1.5.1 → 2.1.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 (79) hide show
  1. package/README.md +59 -45
  2. package/dist/commands/chat.js +10 -2
  3. package/dist/services/agent/slashCommands.js +369 -42
  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 +272 -34
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +288 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +182 -36
  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 +112 -19
  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 +362 -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 +217 -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 +190 -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/services/workspaceRegistry.d.ts +137 -0
  44. package/dist/services/workspaceRegistry.js +270 -0
  45. package/dist/utils/asyncContext.d.ts +16 -0
  46. package/dist/utils/asyncContext.js +25 -0
  47. package/dist/utils/config.d.ts +3 -1
  48. package/dist/utils/config.js +3 -1
  49. package/dist/utils/contextPrompts.js +10 -3
  50. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  52. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  53. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  54. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  56. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  57. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  58. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  59. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  60. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  61. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  62. package/dist/utils/dependencyTracer.js +31 -17
  63. package/dist/utils/excludedExtensions.js +0 -1
  64. package/dist/utils/historyPrompt.d.ts +9 -0
  65. package/dist/utils/historyPrompt.js +87 -0
  66. package/dist/utils/logo.js +7 -7
  67. package/dist/utils/paste.d.ts +21 -0
  68. package/dist/utils/paste.js +22 -1
  69. package/dist/utils/pathSecurity.d.ts +31 -0
  70. package/dist/utils/pathSecurity.js +48 -0
  71. package/dist/utils/profiles.d.ts +2 -0
  72. package/dist/utils/profiles.js +44 -0
  73. package/dist/utils/projectStorage.js +10 -7
  74. package/dist/utils/systemPrompts.d.ts +6 -3
  75. package/dist/utils/systemPrompts.js +111 -6
  76. package/dist/utils/types.d.ts +33 -0
  77. package/dist/utils/types.js +1 -0
  78. package/oclif.manifest.json +2 -2
  79. package/package.json +4 -3
@@ -4,13 +4,14 @@ import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { exec } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
- import { toggleDebugMode } from '../../utils/logger.js';
7
+ import crypto from 'node:crypto';
8
+ import { toggleDebugMode, isDebugOn } from '../../utils/logger.js';
8
9
  import { changeLogger } from '../changeLogger.js';
9
10
  import { chatHistoryService } from '../chatHistoryService.js';
10
11
  import { printLogo, brandBg, brandFg } from '../../utils/logo.js';
11
12
  import { readPaste } from '../../utils/paste.js';
12
- import { setApprovalMode, getApprovalMode } from '../agent-tools.js';
13
- import { ProxyChatSession } from '../ai.js';
13
+ import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled, isSemanticSearchEnabled, setSemanticSearchEnabled, } from '../agent-tools.js';
14
+ import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel, getGlobalLatestUsageMetadata } from '../ai.js';
14
15
  const execAsync = promisify(exec);
15
16
  /**
16
17
  * Handles all slash command operations (/paste, /clear, /models, /debug, /auto-approve, /revert, /commit).
@@ -39,6 +40,16 @@ export async function handleSlashCommand(command, context) {
39
40
  return { shouldContinue: true, isRawPasteMode: false };
40
41
  }
41
42
  }
43
+ if (lowerCommand === '/plan') {
44
+ const newPlanMode = !context.isPlanMode;
45
+ if (newPlanMode) {
46
+ p.log.info(pc.cyan('Plan mode enabled. The AI will formulate a step-by-step plan instead of executing code. Type /plan again to exit.'));
47
+ }
48
+ else {
49
+ p.log.info(pc.cyan('Plan mode disabled. Returning to normal execution.'));
50
+ }
51
+ return { shouldContinue: true, isPlanModeOverride: newPlanMode };
52
+ }
42
53
  if (lowerCommand === '/clear') {
43
54
  chat.clearHistory();
44
55
  process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
@@ -71,10 +82,16 @@ export async function handleSlashCommand(command, context) {
71
82
  label: 'Gemini 3.1 Flash-Lite',
72
83
  hint: 'Ultra-fast and cost-effective',
73
84
  },
85
+ {
86
+ value: 'auto',
87
+ label: 'Auto (Flash-Lite / Flash 3.5)',
88
+ hint: 'Dynamically routes between Flash-Lite and Flash 3.5 based on prompt complexity',
89
+ },
74
90
  ],
75
91
  });
76
92
  if (!p.isCancel(selectedModel)) {
77
93
  chat.setModel(selectedModel);
94
+ setGlobalActiveModel(selectedModel);
78
95
  p.log.success(`Model successfully switched to ${pc.cyan(selectedModel)}`);
79
96
  }
80
97
  return { shouldContinue: true };
@@ -100,41 +117,149 @@ export async function handleSlashCommand(command, context) {
100
117
  }
101
118
  return { shouldContinue: true };
102
119
  }
120
+ if (lowerCommand === '/sub-agents') {
121
+ if (isSubAgentsEnabled()) {
122
+ setSubAgentsEnabled(false);
123
+ p.log.success('MMAAK Engine disabled. Single-agent mode active.');
124
+ }
125
+ else {
126
+ setSubAgentsEnabled(true);
127
+ p.log.success('MMAAK Engine enabled. The system will now use parallel investigation and thread agents.');
128
+ }
129
+ return { shouldContinue: true };
130
+ }
131
+ if (lowerCommand === '/semantic-search') {
132
+ if (isSemanticSearchEnabled()) {
133
+ setSemanticSearchEnabled(false);
134
+ p.log.success('Semantic search disabled. The vector indexing layer is now turned off.');
135
+ }
136
+ else {
137
+ setSemanticSearchEnabled(true);
138
+ p.log.success('Semantic search enabled. The AST-aware vector index is active.');
139
+ }
140
+ return { shouldContinue: true };
141
+ }
142
+ if (lowerCommand === '/stats') {
143
+ const latestUsage = getGlobalLatestUsageMetadata();
144
+ const currentModel = chat.getModel();
145
+ const globalModel = getGlobalActiveModel();
146
+ const displayModel = globalModel === 'auto' ? `Auto (Last turn: ${currentModel})` : currentModel;
147
+ const autoApprove = getApprovalMode() === 'skip-all' ? 'Enabled' : 'Disabled';
148
+ const subAgents = isSubAgentsEnabled() ? 'Enabled' : 'Disabled';
149
+ const semanticSearch = isSemanticSearchEnabled() ? 'Enabled' : 'Disabled';
150
+ const planMode = context.isPlanMode ? 'Enabled' : 'Disabled';
151
+ p.log.step(pc.magenta('📊 Session Statistics & Status'));
152
+ console.log(pc.dim('----------------------------------------'));
153
+ console.log(`${pc.bold('AI Model:')} ${pc.cyan(displayModel)}`);
154
+ console.log(`${pc.bold('Auto-Approve:')} ${autoApprove === 'Enabled' ? pc.green(autoApprove) : pc.yellow(autoApprove)}`);
155
+ console.log(`${pc.bold('Sub-Agents:')} ${subAgents === 'Enabled' ? pc.green(subAgents) : pc.yellow(subAgents)}`);
156
+ console.log(`${pc.bold('Semantic Search:')} ${semanticSearch === 'Enabled' ? pc.green(semanticSearch) : pc.yellow(semanticSearch)}`);
157
+ console.log(`${pc.bold('Plan Mode:')} ${planMode === 'Enabled' ? pc.green(planMode) : pc.yellow(planMode)}`);
158
+ const debugMode = isDebugOn() ? 'Enabled' : 'Disabled';
159
+ console.log(`${pc.bold('Debug Log:')} ${debugMode === 'Enabled' ? pc.green(debugMode) : pc.yellow(debugMode)}`);
160
+ if (latestUsage) {
161
+ if (latestUsage.remainingBalance !== undefined) {
162
+ let diffStr = '';
163
+ if (latestUsage.creditsUsed !== undefined) {
164
+ const before = latestUsage.remainingBalance + latestUsage.creditsUsed;
165
+ console.log(`${pc.bold('Credits Before:')} ${pc.cyan(before.toLocaleString())}`);
166
+ const diff = -latestUsage.creditsUsed;
167
+ if (diff < 0) {
168
+ diffStr = ` ${pc.red(`(${diff})`)}`;
169
+ }
170
+ else if (diff > 0) {
171
+ diffStr = ` ${pc.green(`(+${diff})`)}`;
172
+ }
173
+ else {
174
+ diffStr = ` ${pc.dim(`(0)`)}`;
175
+ }
176
+ }
177
+ console.log(`${pc.bold('Credits Left:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}${diffStr}`);
178
+ }
179
+ const totalInputTokens = (latestUsage.promptTokens || 0) + (latestUsage.cachedTokens || 0);
180
+ if (totalInputTokens > 0) {
181
+ console.log(`${pc.bold('Last Turn Input:')} ${totalInputTokens.toLocaleString()} tokens`);
182
+ }
183
+ if (latestUsage.candidatesTokens) {
184
+ console.log(`${pc.bold('Last Turn Output:')} ${latestUsage.candidatesTokens.toLocaleString()} tokens`);
185
+ }
186
+ }
187
+ else {
188
+ console.log(`${pc.bold('Token Usage:')} No data available yet.`);
189
+ }
190
+ console.log(pc.dim('----------------------------------------\n'));
191
+ return { shouldContinue: true };
192
+ }
103
193
  if (lowerCommand === '/revert') {
194
+ const isEnabled = changeLogger.getIsEnabled();
104
195
  const history = changeLogger.getHistory();
105
- if (!history || history.length === 0) {
106
- p.log.warn('No changes to revert.');
107
- return { shouldContinue: true };
108
- }
109
- const lastChangeSet = history[history.length - 1];
110
196
  const truncate = (str, max) => {
111
197
  const singleLine = str.replace(/\s+/g, ' ').trim();
112
198
  return singleLine.length > max ? singleLine.substring(0, max - 3) + '...' : singleLine;
113
199
  };
200
+ let lastChangeSet = null;
201
+ const options = [];
202
+ if (isEnabled && history && history.length > 0) {
203
+ lastChangeSet = history[history.length - 1];
204
+ options.push({
205
+ value: 'revert_last',
206
+ label: `Revert last change (${truncate(lastChangeSet.description, 50)})${lastChangeSet.status === 'partial' ? ' [Partial]' : ''}`,
207
+ });
208
+ options.push({ value: 'view_history', label: 'View history' });
209
+ }
210
+ if (isEnabled) {
211
+ options.push({ value: 'disable_revert', label: 'Disable' });
212
+ }
213
+ else {
214
+ options.push({ value: 'enable_revert', label: 'Enable' });
215
+ }
216
+ options.push({ value: 'cancel', label: 'Cancel' });
114
217
  const revertMenu = await p['select']({
115
- message: 'Revert Menu',
116
- options: [
117
- {
118
- value: 'revert_last',
119
- label: `Revert last change (${truncate(lastChangeSet.description, 50)})${lastChangeSet.status === 'partial' ? ' [Partial]' : ''}`,
120
- },
121
- { value: 'view_history', label: 'View history' },
122
- { value: 'cancel', label: 'Cancel' },
123
- ],
218
+ message: isEnabled && history.length === 0 ? 'Revert Menu (No changes to revert)' : 'Revert Menu',
219
+ options,
124
220
  });
125
221
  if (p.isCancel(revertMenu) || revertMenu === 'cancel') {
126
222
  return { shouldContinue: true };
127
223
  }
128
- let targetTimestamp = lastChangeSet.timestamp;
224
+ if (revertMenu === 'disable_revert') {
225
+ changeLogger.setIsEnabled(false);
226
+ p.log.success('Revert command has been disabled. File modifications will no longer be tracked.');
227
+ return { shouldContinue: true };
228
+ }
229
+ if (revertMenu === 'enable_revert') {
230
+ changeLogger.setIsEnabled(true);
231
+ p.log.success('Revert command has been enabled. Future file modifications will be tracked.');
232
+ return { shouldContinue: true };
233
+ }
234
+ let targetTimestamp = lastChangeSet ? lastChangeSet.timestamp : null;
129
235
  if (revertMenu === 'view_history') {
236
+ const { workspaceRegistry } = await import('../workspaceRegistry.js');
130
237
  const historyOptions = history
131
238
  .slice()
132
239
  .reverse()
133
- .map((cs, i) => ({
134
- value: cs.timestamp,
135
- label: `[${i === 0 ? 'Latest' : `-${i}`}] ${truncate(cs.description, 50)} (${new Date(cs.timestamp).toLocaleTimeString()})${cs.status === 'partial' ? ' [Partial]' : ''}`,
136
- hint: `Reverts this and all ${i} changes after it`,
137
- }));
240
+ .map((cs, i) => {
241
+ const externalWorkspaces = new Set();
242
+ for (const change of cs.changes) {
243
+ if (change.filePath.startsWith('@')) {
244
+ const slashIndex = change.filePath.indexOf('/');
245
+ const aliasStr = slashIndex === -1 ? change.filePath.substring(1) : change.filePath.substring(1, slashIndex);
246
+ const ws = workspaceRegistry.get(aliasStr);
247
+ if (ws) {
248
+ const rootName = path.basename(ws.absolutePath);
249
+ externalWorkspaces.add(rootName);
250
+ }
251
+ else {
252
+ externalWorkspaces.add(`@${aliasStr}`);
253
+ }
254
+ }
255
+ }
256
+ const tags = externalWorkspaces.size > 0 ? pc.blue(` [${Array.from(externalWorkspaces).join(', ')}]`) : '';
257
+ return {
258
+ value: cs.timestamp,
259
+ label: `[${i === 0 ? 'Latest' : `-${i}`}] ${truncate(cs.description, 50)}${tags} (${new Date(cs.timestamp).toLocaleTimeString()})${cs.status === 'partial' ? ' [Partial]' : ''}`,
260
+ hint: `Reverts this and all ${i} changes after it`,
261
+ };
262
+ });
138
263
  const selectedHistory = await p['select']({
139
264
  message: 'Select the point in history to revert back to:',
140
265
  options: [...historyOptions, { value: -1, label: 'Cancel' }],
@@ -149,13 +274,14 @@ export async function handleSlashCommand(command, context) {
149
274
  try {
150
275
  const changesToRevert = changeLogger.popUntil(targetTimestamp);
151
276
  const flatChanges = changesToRevert.flatMap((cs) => cs.changes);
277
+ const { resolveAndValidateMultiWorkspacePath } = await import('../../utils/pathSecurity.js');
152
278
  for (const change of flatChanges) {
153
- const absPath = path.resolve(workspaceRoot, change.filePath);
279
+ const { absolutePath } = resolveAndValidateMultiWorkspacePath(workspaceRoot, change.filePath);
154
280
  if (change.action === 'create') {
155
- await fs.rm(absPath, { force: true });
281
+ await fs.rm(absolutePath, { force: true });
156
282
  }
157
283
  else if (change.originalContent !== null) {
158
- await fs.writeFile(absPath, change.originalContent, 'utf-8');
284
+ await fs.writeFile(absolutePath, change.originalContent, 'utf-8');
159
285
  }
160
286
  }
161
287
  spinner.stop('Reverted successfully.');
@@ -168,33 +294,75 @@ export async function handleSlashCommand(command, context) {
168
294
  return { shouldContinue: true };
169
295
  }
170
296
  if (lowerCommand === '/chats') {
171
- const sessions = chatHistoryService.getSessions();
172
- if (!sessions || sessions.length === 0) {
173
- p.log.warn('No chat history found.');
174
- return { shouldContinue: true };
297
+ const sessions = chatHistoryService.getSessions() || [];
298
+ const hasSessions = sessions.length > 0;
299
+ const options = [{ value: 'new', label: 'New Chat' }];
300
+ if (hasSessions) {
301
+ options.push({ value: 'resume', label: 'View Chats' });
302
+ options.push({ value: 'delete', label: 'Delete Chat' });
175
303
  }
304
+ options.push({ value: 'cancel', label: 'Cancel' });
176
305
  const truncate = (str, max) => {
177
306
  const singleLine = str.replace(/\s+/g, ' ').trim();
178
307
  return singleLine.length > max ? singleLine.substring(0, max - 3) + '...' : singleLine;
179
308
  };
180
309
  const chatsMenu = await p['select']({
181
310
  message: 'Chat Sessions',
182
- options: [
183
- { value: 'resume', label: 'Resume another chat' },
184
- { value: 'delete', label: 'Delete a past chat' },
185
- { value: 'cancel', label: 'Cancel' },
186
- ],
311
+ options,
187
312
  });
188
313
  if (p.isCancel(chatsMenu) || chatsMenu === 'cancel') {
189
314
  return { shouldContinue: true };
190
315
  }
316
+ if (chatsMenu === 'new') {
317
+ chat.clearHistory();
318
+ if (chatSessionState) {
319
+ chatSessionState.id = crypto.randomUUID();
320
+ chatSessionState.title = '';
321
+ }
322
+ process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
323
+ printLogo();
324
+ p.intro(`${brandBg(' Minovative Mind CLI ')} ${pc.dim('v' + version)}`);
325
+ p.log.info(`${pc.dim('Workspace:')} ${brandFg(workspaceRoot)}`);
326
+ p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
327
+ p.log.success('Started a new chat session.');
328
+ console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
329
+ return { shouldContinue: true };
330
+ }
331
+ const { workspaceRegistry } = await import('../workspaceRegistry.js');
191
332
  const sessionOptions = sessions
192
333
  .slice()
193
334
  .reverse()
194
- .map((s) => ({
195
- value: s.id,
196
- label: `${truncate(s.title, 50)} (${new Date(s.timestamp).toLocaleString()})`,
197
- }));
335
+ .map((s) => {
336
+ const externalWorkspaces = new Set();
337
+ if (s.history) {
338
+ for (const item of s.history) {
339
+ if (item.role === 'user' || !item.parts)
340
+ continue;
341
+ for (const part of item.parts) {
342
+ if (part.functionCall && part.functionCall.args && part.functionCall.args.filePath) {
343
+ const p = part.functionCall.args.filePath;
344
+ if (typeof p === 'string' && p.startsWith('@')) {
345
+ const slashIndex = p.indexOf('/');
346
+ const aliasStr = slashIndex === -1 ? p.substring(1) : p.substring(1, slashIndex);
347
+ const ws = workspaceRegistry.get(aliasStr);
348
+ if (ws) {
349
+ const rootName = path.basename(ws.absolutePath);
350
+ externalWorkspaces.add(rootName);
351
+ }
352
+ else {
353
+ externalWorkspaces.add(`@${aliasStr}`);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ const tags = externalWorkspaces.size > 0 ? pc.blue(` [${Array.from(externalWorkspaces).join(', ')}]`) : '';
361
+ return {
362
+ value: s.id,
363
+ label: `${truncate(s.title, 50)}${tags} (${new Date(s.timestamp).toLocaleString()})`,
364
+ };
365
+ });
198
366
  if (chatsMenu === 'resume') {
199
367
  const selectedSessionId = await p['select']({
200
368
  message: 'Select a session to resume:',
@@ -219,13 +387,19 @@ export async function handleSlashCommand(command, context) {
219
387
  // Print the loaded history so the user can see past context
220
388
  for (const item of session.history) {
221
389
  if (item.role === 'user') {
222
- const text = item.parts.map((p) => p.text).filter(Boolean).join('\n');
390
+ const text = item.parts
391
+ .map((p) => p.text)
392
+ .filter(Boolean)
393
+ .join('');
223
394
  if (text) {
224
- p.log.step(pc.cyan(text));
395
+ p.log.step(pc.bgBlue(pc.white(` ${text} `)));
225
396
  }
226
397
  }
227
398
  else if (item.role === 'model') {
228
- const text = item.parts.map((p) => p.text).filter(Boolean).join('\n');
399
+ const text = item.parts
400
+ .map((p) => p.text)
401
+ .filter(Boolean)
402
+ .join('');
229
403
  if (text) {
230
404
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim('(Resumed)')}\n`);
231
405
  const { marked } = await import('marked');
@@ -260,6 +434,159 @@ export async function handleSlashCommand(command, context) {
260
434
  }
261
435
  return { shouldContinue: true };
262
436
  }
437
+ if (lowerCommand === '/workspaces') {
438
+ const { workspaceRegistry } = await import('../workspaceRegistry.js');
439
+ while (true) {
440
+ const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
441
+ const options = [];
442
+ options.push({ value: 'add', label: 'Add Workspace' });
443
+ const externalRoots = allRoots.filter(r => r.alias);
444
+ if (externalRoots.length > 0) {
445
+ options.push({ value: 'edit', label: 'Edit Workspace' });
446
+ options.push({ value: 'remove', label: 'Remove Workspace' });
447
+ options.push({ value: 'list', label: 'List Workspaces' });
448
+ }
449
+ options.push({ value: 'cancel', label: 'Exit Menu' });
450
+ const action = await p['select']({
451
+ message: 'Manage External Workspaces',
452
+ options,
453
+ });
454
+ if (p.isCancel(action) || action === 'cancel') {
455
+ break;
456
+ }
457
+ if (action === 'add') {
458
+ const aliasStr = await p['text']({
459
+ message: 'Enter a short alias (e.g. backend, ui):',
460
+ validate: (val) => {
461
+ if (!val)
462
+ return 'Alias is required';
463
+ if (!/^[a-zA-Z0-9_-]+$/.test(val))
464
+ return 'Only letters, numbers, hyphens, and underscores';
465
+ if (workspaceRegistry.getAllRoots(workspaceRoot).some(r => r.alias === val))
466
+ return 'Alias already in use';
467
+ }
468
+ });
469
+ if (p.isCancel(aliasStr))
470
+ continue;
471
+ const rootPathStr = await p['text']({
472
+ message: 'Enter the absolute path to the workspace root (e.g. /Users/name/Projects/app):',
473
+ validate: (val) => {
474
+ if (!val)
475
+ return 'Path is required';
476
+ }
477
+ });
478
+ if (p.isCancel(rootPathStr))
479
+ continue;
480
+ let cleanRootPathStr = rootPathStr.trim();
481
+ if ((cleanRootPathStr.startsWith("'") && cleanRootPathStr.endsWith("'")) ||
482
+ (cleanRootPathStr.startsWith('"') && cleanRootPathStr.endsWith('"'))) {
483
+ cleanRootPathStr = cleanRootPathStr.slice(1, -1);
484
+ }
485
+ try {
486
+ const stats = await fs.stat(cleanRootPathStr);
487
+ if (!stats.isDirectory()) {
488
+ p.log.error('Path is not a directory.');
489
+ continue;
490
+ }
491
+ workspaceRegistry.register(aliasStr, cleanRootPathStr);
492
+ p.log.success(`Added @${aliasStr} -> ${cleanRootPathStr}`);
493
+ }
494
+ catch (e) {
495
+ p.log.error(`Invalid path or directory does not exist: ${cleanRootPathStr}`);
496
+ }
497
+ }
498
+ else if (action === 'edit') {
499
+ const editOptions = externalRoots.map(r => ({
500
+ value: r.alias,
501
+ label: `@${r.alias} -> ${r.root}`
502
+ }));
503
+ editOptions.push({ value: 'cancel', label: 'Cancel' });
504
+ const aliasToEdit = await p['select']({
505
+ message: 'Select workspace to edit:',
506
+ options: editOptions
507
+ });
508
+ if (p.isCancel(aliasToEdit) || aliasToEdit === 'cancel')
509
+ continue;
510
+ const ws = workspaceRegistry.get(aliasToEdit);
511
+ if (!ws)
512
+ continue;
513
+ const newAliasStr = await p['text']({
514
+ message: `Enter new alias (current: ${ws.alias}):`,
515
+ initialValue: ws.alias,
516
+ validate: (val) => {
517
+ if (!val)
518
+ return 'Alias is required';
519
+ if (!/^[a-zA-Z0-9_-]+$/.test(val))
520
+ return 'Only letters, numbers, hyphens, and underscores';
521
+ if (val !== ws.alias && workspaceRegistry.getAllRoots(workspaceRoot).some(r => r.alias === val))
522
+ return 'Alias already in use';
523
+ }
524
+ });
525
+ if (p.isCancel(newAliasStr))
526
+ continue;
527
+ const newRootPathStr = await p['text']({
528
+ message: `Enter absolute path to workspace root (e.g. /Users/name/Projects/app):`,
529
+ initialValue: ws.absolutePath,
530
+ validate: (val) => {
531
+ if (!val)
532
+ return 'Path is required';
533
+ }
534
+ });
535
+ if (p.isCancel(newRootPathStr))
536
+ continue;
537
+ let cleanNewRootPathStr = newRootPathStr.trim();
538
+ if ((cleanNewRootPathStr.startsWith("'") && cleanNewRootPathStr.endsWith("'")) ||
539
+ (cleanNewRootPathStr.startsWith('"') && cleanNewRootPathStr.endsWith('"'))) {
540
+ cleanNewRootPathStr = cleanNewRootPathStr.slice(1, -1);
541
+ }
542
+ try {
543
+ const stats = await fs.stat(cleanNewRootPathStr);
544
+ if (!stats.isDirectory()) {
545
+ p.log.error('Path is not a directory.');
546
+ continue;
547
+ }
548
+ // Remove old alias first to avoid duplicate alias error, or to clean up
549
+ workspaceRegistry.unregister(ws.alias);
550
+ workspaceRegistry.register(newAliasStr, cleanNewRootPathStr);
551
+ p.log.success(`Updated @${newAliasStr} -> ${cleanNewRootPathStr}`);
552
+ }
553
+ catch (e) {
554
+ // If register failed, try to rollback
555
+ p.log.error(`Failed to update workspace: ${e instanceof Error ? e.message : String(e)}`);
556
+ try {
557
+ workspaceRegistry.register(ws.alias, ws.absolutePath);
558
+ }
559
+ catch { }
560
+ }
561
+ }
562
+ else if (action === 'remove') {
563
+ const removeOptions = externalRoots.map(r => ({
564
+ value: r.alias,
565
+ label: `@${r.alias} -> ${r.root}`
566
+ }));
567
+ removeOptions.push({ value: 'cancel', label: 'Cancel' });
568
+ const aliasToRemove = await p['select']({
569
+ message: 'Select workspace to remove:',
570
+ options: removeOptions
571
+ });
572
+ if (p.isCancel(aliasToRemove) || aliasToRemove === 'cancel')
573
+ continue;
574
+ workspaceRegistry.unregister(aliasToRemove);
575
+ p.log.success(`Removed @${aliasToRemove}`);
576
+ }
577
+ else if (action === 'list') {
578
+ for (const { alias, root } of allRoots) {
579
+ if (alias) {
580
+ p.log.step(`${pc.blue(`@${alias}`)} -> ${pc.dim(root)}`);
581
+ }
582
+ else {
583
+ p.log.step(`${pc.cyan('(primary)')} -> ${pc.dim(root)}`);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ return { shouldContinue: true };
589
+ }
263
590
  if (lowerCommand === '/commit') {
264
591
  const commitSpinner = p.spinner();
265
592
  commitSpinner.start('Staging changes and analyzing diff...');
@@ -25,7 +25,7 @@ export declare function formatToolCall(name: string, args: Record<string, unknow
25
25
  * 6. **Submit Loop Frame**: Feeds execution outcomes back to the Gemini session and recursively repeats this sequence until the LLM produces a final text answer without further tool requests.
26
26
  *
27
27
  * ### Limits & Recovery Guardrails:
28
- * - **Turn-Limiting**: Capped at `MAX_TURNS` (100) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
28
+ * - **Turn-Limiting**: Capped at `MAX_TURNS` (Infinity) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
29
29
  * - **Empty-Response Healing**: If the API returns an empty text response with no tools, the system initiates up to `MAX_EMPTY_RETRIES` (3) system-driven wake-up prompts to re-engage the model.
30
30
  *
31
31
  * @param chat - The current active proxy chat session.
@@ -5,6 +5,7 @@ import { executeTool } from '../agent-tools.js';
5
5
  import { getPlanExecutionConfig } from '../ai.js';
6
6
  import { routeIntent } from '../contextAgent.js';
7
7
  import { requestCommandApproval } from './commandApproval.js';
8
+ import { getMetricCollector } from '../metrics.js';
8
9
  // ─── Constants ───────────────────────────────────────────────────────
9
10
  /**
10
11
  * Mapping of tool identifiers to user-friendly terminal emojis.
@@ -92,7 +93,7 @@ export function formatToolCall(name, args) {
92
93
  * 6. **Submit Loop Frame**: Feeds execution outcomes back to the Gemini session and recursively repeats this sequence until the LLM produces a final text answer without further tool requests.
93
94
  *
94
95
  * ### Limits & Recovery Guardrails:
95
- * - **Turn-Limiting**: Capped at `MAX_TURNS` (100) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
96
+ * - **Turn-Limiting**: Capped at `MAX_TURNS` (Infinity) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
96
97
  * - **Empty-Response Healing**: If the API returns an empty text response with no tools, the system initiates up to `MAX_EMPTY_RETRIES` (3) system-driven wake-up prompts to re-engage the model.
97
98
  *
98
99
  * @param chat - The current active proxy chat session.
@@ -107,7 +108,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
107
108
  let response = result.response;
108
109
  // Upper limit on autonomous sequential tool executions to prevent out-of-control loops
109
110
  let turnCount = 0;
110
- const MAX_TURNS = 100;
111
+ const MAX_TURNS = Infinity;
111
112
  // Recovery thresholds for handling unexpected empty API payloads
112
113
  let emptyRetryCount = 0;
113
114
  const MAX_EMPTY_RETRIES = 3;
@@ -170,6 +171,9 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
170
171
  return finalOutput;
171
172
  }
172
173
  // Process each tool call requested by the model (using helper to avoid nested loop warning)
174
+ const collector = getMetricCollector();
175
+ if (collector)
176
+ collector.recordToolTurn();
173
177
  const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal);
174
178
  if (execRes.aborted) {
175
179
  return '[Generation stopped by user]';
@@ -263,6 +267,7 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
263
267
  response: {
264
268
  output: toolResult.output,
265
269
  ...(toolResult.error ? { error: toolResult.error } : {}),
270
+ ...(toolResult.inlineData ? { inlineData: toolResult.inlineData } : {}),
266
271
  },
267
272
  },
268
273
  });
@@ -9,6 +9,7 @@ export interface SlashCommandContext {
9
9
  workspaceRoot: string;
10
10
  version: string;
11
11
  isRawPasteMode: boolean;
12
+ isPlanMode: boolean;
12
13
  chatSessionState: {
13
14
  id: string;
14
15
  title: string;
@@ -18,4 +19,5 @@ export interface SlashCommandResult {
18
19
  shouldContinue: boolean;
19
20
  userInputOverride?: string;
20
21
  isRawPasteMode?: boolean;
22
+ isPlanModeOverride?: boolean;
21
23
  }
@@ -2,7 +2,14 @@ import { type FunctionDeclaration } from '@google/generative-ai';
2
2
  export interface ToolResult {
3
3
  error?: string;
4
4
  output: string;
5
+ inlineData?: {
6
+ mimeType: string;
7
+ data: string;
8
+ };
5
9
  }
10
+ export declare function isSemanticSearchEnabled(): boolean;
11
+ export declare function setSemanticSearchEnabled(val: boolean): void;
12
+ export declare function getToolDeclarations(): FunctionDeclaration[];
6
13
  /**
7
14
  * FunctionDeclaration-compatible schema objects that describe
8
15
  * every tool the agent can invoke. Passed to the model at init.
@@ -11,6 +18,8 @@ export declare const toolDeclarations: FunctionDeclaration[];
11
18
  export type ApprovalMode = 'ask' | 'skip-once' | 'skip-all';
12
19
  export declare function getApprovalMode(): ApprovalMode;
13
20
  export declare function setApprovalMode(mode: ApprovalMode): void;
21
+ export declare function isSubAgentsEnabled(): boolean;
22
+ export declare function setSubAgentsEnabled(enabled: boolean): void;
14
23
  /**
15
24
  * If set to 'skip-once', reverts to 'ask' after a single command is run.
16
25
  */
@@ -29,8 +38,4 @@ export declare function grepSearch(workspaceRoot: string, pattern: string, fileG
29
38
  export declare function traceDependencies(workspaceRoot: string, filePath: string, direction?: string, maxDepth?: number): Promise<ToolResult>;
30
39
  export declare function findRecentChanges(workspaceRoot: string, dirPath?: string, minutes?: number, maxDepth?: number): Promise<ToolResult>;
31
40
  export declare function runDebugScript(workspaceRoot: string, language: string, code: string, abortSignal?: AbortSignal): Promise<ToolResult>;
32
- /**
33
- * Dispatches a function call from the model to the appropriate local tool.
34
- * Returns the tool result as a string to feed back to the model.
35
- */
36
41
  export declare function executeTool(workspaceRoot: string, toolName: string, args: Record<string, unknown>, abortSignal?: AbortSignal): Promise<ToolResult>;