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.
- package/README.md +59 -45
- package/dist/commands/chat.js +10 -2
- package/dist/services/agent/slashCommands.js +369 -42
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +272 -34
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +288 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +182 -36
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +112 -19
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +362 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +217 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +190 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/services/workspaceRegistry.d.ts +137 -0
- package/dist/services/workspaceRegistry.js +270 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +10 -3
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.js +7 -7
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/pathSecurity.d.ts +31 -0
- package/dist/utils/pathSecurity.js +48 -0
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +111 -6
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
279
|
+
const { absolutePath } = resolveAndValidateMultiWorkspacePath(workspaceRoot, change.filePath);
|
|
154
280
|
if (change.action === 'create') {
|
|
155
|
-
await fs.rm(
|
|
281
|
+
await fs.rm(absolutePath, { force: true });
|
|
156
282
|
}
|
|
157
283
|
else if (change.originalContent !== null) {
|
|
158
|
-
await fs.writeFile(
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
390
|
+
const text = item.parts
|
|
391
|
+
.map((p) => p.text)
|
|
392
|
+
.filter(Boolean)
|
|
393
|
+
.join('');
|
|
223
394
|
if (text) {
|
|
224
|
-
p.log.step(pc.
|
|
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
|
|
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` (
|
|
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` (
|
|
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 =
|
|
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>;
|