minovative-mind-cli 2.4.0 → 2.5.1
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 +5 -0
- package/dist/commands/chat.js +1 -0
- package/dist/services/agent/slashCommands.js +106 -2
- package/dist/services/agent/syntaxAgent.d.ts +10 -0
- package/dist/services/agent/syntaxAgent.js +52 -0
- package/dist/services/agent/toolLoop.js +5 -17
- package/dist/services/agent-tools.js +89 -28
- package/dist/services/agent.js +1 -0
- package/dist/services/ai.d.ts +1 -1
- package/dist/services/ai.js +128 -13
- package/dist/services/changeLogger.js +2 -2
- package/dist/services/contextAgent.js +5 -35
- package/dist/services/metrics.d.ts +6 -8
- package/dist/services/orchestration/investigationAgent.js +0 -32
- package/dist/services/orchestration/investigationOrchestrator.js +1 -1
- package/dist/services/orchestration/messageBus.d.ts +1 -1
- package/dist/services/orchestration/messageBus.js +14 -14
- package/dist/services/orchestration/readCache.js +1 -1
- package/dist/services/orchestration/scopedTools.js +2 -1
- package/dist/services/orchestration/subAgent.js +13 -26
- package/dist/services/proxyClient.d.ts +8 -0
- package/dist/services/proxyClient.js +17 -0
- package/dist/services/verificationService.js +15 -6
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/credentialStore.d.ts +7 -0
- package/dist/utils/credentialStore.js +8 -0
- package/dist/utils/localSyntaxValidator.d.ts +9 -0
- package/dist/utils/localSyntaxValidator.js +103 -0
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +3 -6
- package/oclif.manifest.json +2 -2
- package/package.json +1 -2
- package/dist/utils/syntaxValidator.d.ts +0 -5
- package/dist/utils/syntaxValidator.js +0 -81
package/README.md
CHANGED
|
@@ -93,6 +93,11 @@ Hot-swap during a session using `/models`:
|
|
|
93
93
|
| **Gemini 3.1 Pro** | Complex architectural changes |
|
|
94
94
|
| **Gemini 3.5 Flash Lite** | Best for speed and cost efficiency |
|
|
95
95
|
|
|
96
|
+
### BYOK (Bring Your Own Key)
|
|
97
|
+
If you prefer to use your own Gemini API key, you can configure it via the settings.
|
|
98
|
+
* **Configuration:** Set `useByok: true` and `geminiApiKey: "YOUR_KEY"` in your credentials storage.
|
|
99
|
+
* **Error Handling:** If your key is invalid, expired, or you hit rate limits, the CLI will report a `BYOK AI Error`. Please check your API key status in the Google AI Studio dashboard.
|
|
100
|
+
|
|
96
101
|
> Background tasks (routing, compression, commits) always use lightweight
|
|
97
102
|
> models automatically. You pay for those lightweight model
|
|
98
103
|
> background ai models and for your selected model
|
package/dist/commands/chat.js
CHANGED
|
@@ -30,6 +30,7 @@ Inside the chat session, you can use the following commands in the slash menu:
|
|
|
30
30
|
/auto-approve - Toggle automatic approval of tool/command runs
|
|
31
31
|
/sub-agents - Toggle the MMAAK Engine for parallel investigation and execution
|
|
32
32
|
/workspaces - Manage external workspaces for cross-project development
|
|
33
|
+
/config-key - BYOK (Bring Your Own Key) Configuration
|
|
33
34
|
/stats - View current session statistics and configuration
|
|
34
35
|
/commit - Commit current workspace changes to Git
|
|
35
36
|
/revert - Revert the last file modification made by the agent
|
|
@@ -11,8 +11,11 @@ import { chatHistoryService } from '../chatHistoryService.js';
|
|
|
11
11
|
import { printLogo, brandBg, brandFg } from '../../utils/logo.js';
|
|
12
12
|
import { readPaste } from '../../utils/paste.js';
|
|
13
13
|
import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled } from '../agent-tools.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { ProxyClient, getAndResetTurnUsage } from '../proxyClient.js';
|
|
15
|
+
import { getAuthorizedIdToken } from '../auth.js';
|
|
16
|
+
import { GEMINI_MODELS, isByokEnabled } from '../../utils/config.js';
|
|
17
|
+
import { loadCredentials, updateCredentialField } from '../../utils/credentialStore.js';
|
|
18
|
+
import { getGlobalActiveModel, setGlobalActiveModel, ProxyChatSession } from '../ai.js';
|
|
16
19
|
const execAsync = promisify(exec);
|
|
17
20
|
/**
|
|
18
21
|
* Handles all slash command operations (/paste, /clear, /models, /debug, /auto-approve, /revert, /commit).
|
|
@@ -21,6 +24,44 @@ const execAsync = promisify(exec);
|
|
|
21
24
|
export async function handleSlashCommand(command, context) {
|
|
22
25
|
const { chat, inputHandler, workspaceRoot, version, chatSessionState } = context;
|
|
23
26
|
const lowerCommand = command.toLowerCase();
|
|
27
|
+
if (lowerCommand === '/config-key') {
|
|
28
|
+
const creds = await loadCredentials();
|
|
29
|
+
const action = await p['select']({
|
|
30
|
+
message: 'BYOK (Bring Your Own Key) Configuration',
|
|
31
|
+
options: [
|
|
32
|
+
{ value: 'status', label: 'Status' },
|
|
33
|
+
{ value: 'toggle', label: creds.useByok ? 'Disable BYOK' : 'Enable BYOK' },
|
|
34
|
+
{ value: 'set', label: 'Set API Key' },
|
|
35
|
+
{ value: 'clear', label: 'Clear API Key' },
|
|
36
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
if (action === 'status') {
|
|
40
|
+
const enabled = await isByokEnabled();
|
|
41
|
+
p.log.info(`BYOK Status: ${enabled ? pc.green('Enabled') : pc.red('Disabled')}\n` +
|
|
42
|
+
`API Key: ${creds.geminiApiKey ? pc.green('Set') : pc.red('Not Set')}`);
|
|
43
|
+
}
|
|
44
|
+
else if (action === 'toggle') {
|
|
45
|
+
await updateCredentialField('useByok', !creds.useByok);
|
|
46
|
+
p.log.success(`BYOK ${!creds.useByok ? 'enabled' : 'disabled'}.`);
|
|
47
|
+
}
|
|
48
|
+
else if (action === 'set') {
|
|
49
|
+
const key = await p['text']({
|
|
50
|
+
message: 'Enter your Gemini API Key:',
|
|
51
|
+
validate: (value) => (value ? undefined : 'API Key is required'),
|
|
52
|
+
});
|
|
53
|
+
if (key && typeof key === 'string') {
|
|
54
|
+
await updateCredentialField('geminiApiKey', key);
|
|
55
|
+
p.log.success('API Key updated.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (action === 'clear') {
|
|
59
|
+
await updateCredentialField('geminiApiKey', '');
|
|
60
|
+
await updateCredentialField('useByok', false);
|
|
61
|
+
p.log.success('API Key cleared and BYOK disabled.');
|
|
62
|
+
}
|
|
63
|
+
return { shouldContinue: true };
|
|
64
|
+
}
|
|
24
65
|
if (lowerCommand === '/paste') {
|
|
25
66
|
p.log.info(pc.cyan('Paste mode activated. Paste your text below, then press Ctrl+D on an empty line to submit. (Ctrl+C to cancel)'));
|
|
26
67
|
try {
|
|
@@ -155,12 +196,14 @@ export async function handleSlashCommand(command, context) {
|
|
|
155
196
|
const autoApprove = getApprovalMode() === 'skip-all' ? 'Enabled' : 'Disabled';
|
|
156
197
|
const subAgents = isSubAgentsEnabled() ? 'Enabled' : 'Disabled';
|
|
157
198
|
const planMode = context.isPlanMode ? 'Enabled' : 'Disabled';
|
|
199
|
+
const byokEnabled = await isByokEnabled() ? 'Enabled' : 'Disabled';
|
|
158
200
|
p.log.step(pc.magenta('📊 Session Statistics & Status'));
|
|
159
201
|
console.log(pc.dim('----------------------------------------'));
|
|
160
202
|
console.log(`${pc.bold('AI Model:')} ${pc.cyan(displayModel)}`);
|
|
161
203
|
console.log(`${pc.bold('Auto-Approve:')} ${autoApprove === 'Enabled' ? pc.green(autoApprove) : pc.yellow(autoApprove)}`);
|
|
162
204
|
console.log(`${pc.bold('Sub-Agents:')} ${subAgents === 'Enabled' ? pc.green(subAgents) : pc.yellow(subAgents)}`);
|
|
163
205
|
console.log(`${pc.bold('Plan Mode:')} ${planMode === 'Enabled' ? pc.green(planMode) : pc.yellow(planMode)}`);
|
|
206
|
+
console.log(`${pc.bold('BYOK:')} ${byokEnabled === 'Enabled' ? pc.green(byokEnabled) : pc.yellow(byokEnabled)}`);
|
|
164
207
|
const debugMode = isDebugOn() ? 'Enabled' : 'Disabled';
|
|
165
208
|
console.log(`${pc.bold('Debug Log:')} ${debugMode === 'Enabled' ? pc.green(debugMode) : pc.yellow(debugMode)}`);
|
|
166
209
|
try {
|
|
@@ -835,5 +878,66 @@ ${diffOut}
|
|
|
835
878
|
}
|
|
836
879
|
return { shouldContinue: true };
|
|
837
880
|
}
|
|
881
|
+
if (lowerCommand === '/config-key') {
|
|
882
|
+
const creds = await loadCredentials();
|
|
883
|
+
const byokEnabled = !!(creds.useByok && creds.geminiApiKey);
|
|
884
|
+
const action = await p['select']({
|
|
885
|
+
message: `API Key Configuration (Current Mode: ${byokEnabled ? pc.green('BYOK') : pc.yellow('Credits')})`,
|
|
886
|
+
options: [
|
|
887
|
+
{ value: 'toggle', label: byokEnabled ? 'Disable BYOK (Use Credits)' : 'Enable BYOK (Use API Key)' },
|
|
888
|
+
{ value: 'set', label: 'Set/Update API Key' },
|
|
889
|
+
{ value: 'clear', label: 'Clear API Key' },
|
|
890
|
+
{ value: 'status', label: 'Check Status' },
|
|
891
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
892
|
+
],
|
|
893
|
+
});
|
|
894
|
+
if (p.isCancel(action) || action === 'cancel')
|
|
895
|
+
return { shouldContinue: true };
|
|
896
|
+
if (action === 'toggle') {
|
|
897
|
+
if (!byokEnabled && !creds.geminiApiKey) {
|
|
898
|
+
p.log.error('You must set an API key before enabling BYOK mode.');
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
await updateCredentialField('useByok', !byokEnabled);
|
|
902
|
+
p.log.success(`BYOK mode is now ${!byokEnabled ? pc.green('Enabled') : pc.yellow('Disabled')}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else if (action === 'set') {
|
|
906
|
+
const key = await p['password']({
|
|
907
|
+
message: 'Enter your Google AI Studio API Key:',
|
|
908
|
+
validate: (v) => (!v ? 'API key is required' : undefined),
|
|
909
|
+
});
|
|
910
|
+
if (p.isCancel(key))
|
|
911
|
+
return { shouldContinue: true };
|
|
912
|
+
const spinner = p.spinner();
|
|
913
|
+
spinner.start('Validating API Key...');
|
|
914
|
+
try {
|
|
915
|
+
// Test call to verify key
|
|
916
|
+
const client = new ProxyClient();
|
|
917
|
+
const idToken = (await getAuthorizedIdToken()) || '';
|
|
918
|
+
// We use a simple prompt to test the key
|
|
919
|
+
await client.generateFunctionCallViaProxy(idToken, 'gemini-3.5-flash-lite', [{ role: 'user', parts: [{ text: 'ping' }] }], undefined, undefined, 'You are a validator. Respond with "pong".', { maxOutputTokens: 10 });
|
|
920
|
+
// Reset metrics after validation
|
|
921
|
+
getAndResetTurnUsage();
|
|
922
|
+
await updateCredentialField('geminiApiKey', key);
|
|
923
|
+
spinner.stop('API Key validated and updated.');
|
|
924
|
+
p.log.success('API key saved.');
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
spinner.stop('Validation failed.');
|
|
928
|
+
p.log.error(`Invalid API Key: ${err.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else if (action === 'clear') {
|
|
932
|
+
await updateCredentialField('geminiApiKey', undefined);
|
|
933
|
+
await updateCredentialField('useByok', false);
|
|
934
|
+
p.log.success('API key cleared and BYOK mode disabled.');
|
|
935
|
+
}
|
|
936
|
+
else if (action === 'status') {
|
|
937
|
+
p.log.info(`Mode: ${byokEnabled ? pc.green('BYOK') : pc.yellow('Credits')}\n` +
|
|
938
|
+
`Key Set: ${creds.geminiApiKey ? pc.green('Yes') : pc.red('No')}`);
|
|
939
|
+
}
|
|
940
|
+
return { shouldContinue: true };
|
|
941
|
+
}
|
|
838
942
|
return { shouldContinue: true };
|
|
839
943
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates file content and attempts to fix syntax errors.
|
|
3
|
+
* Uses GEMINI_MODELS.FLASH to perform language-agnostic syntax repair.
|
|
4
|
+
*
|
|
5
|
+
* @param content The file content to validate.
|
|
6
|
+
* @param filePath The path of the file being validated.
|
|
7
|
+
* @param error Optional error message from local validation to guide the AI.
|
|
8
|
+
* @returns The fixed content, or undefined if it was completely valid or couldn't be fixed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateAndFixSyntax(content: string, filePath: string, error?: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ProxyChatSession } from '../ai.js';
|
|
2
|
+
import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validates file content and attempts to fix syntax errors.
|
|
5
|
+
* Uses GEMINI_MODELS.FLASH to perform language-agnostic syntax repair.
|
|
6
|
+
*
|
|
7
|
+
* @param content The file content to validate.
|
|
8
|
+
* @param filePath The path of the file being validated.
|
|
9
|
+
* @param error Optional error message from local validation to guide the AI.
|
|
10
|
+
* @returns The fixed content, or undefined if it was completely valid or couldn't be fixed.
|
|
11
|
+
*/
|
|
12
|
+
export async function validateAndFixSyntax(content, filePath, error) {
|
|
13
|
+
const model = GEMINI_MODELS.FLASH; // Always use flash for syntax checking for speed
|
|
14
|
+
const systemInstruction = `
|
|
15
|
+
<identity>
|
|
16
|
+
You are an expert compiler and syntax validator.
|
|
17
|
+
Your sole job is to read source code and verify its syntax.
|
|
18
|
+
</identity>
|
|
19
|
+
|
|
20
|
+
<rules>
|
|
21
|
+
1. If the provided code has NO syntax errors (e.g., balanced braces, correct language keywords, complete statements), you MUST respond exactly with the word: VALID
|
|
22
|
+
2. If the code has syntax errors (e.g., a missing closing bracket, dangling parenthesis, unfinished string literal), you must fix the code.
|
|
23
|
+
3. If you fix the code, you MUST output ONLY the completely fixed file content. Do not include any explanations, markdown code blocks (like \`\`\`typescript), or conversational text.
|
|
24
|
+
4. Keep the original formatting and comments intact. Only fix the syntax.
|
|
25
|
+
</rules>
|
|
26
|
+
`;
|
|
27
|
+
const chat = new ProxyChatSession(model, systemInstruction, [], // No tools needed
|
|
28
|
+
{
|
|
29
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
30
|
+
temperature: 0.1, // Very low temp for strict syntax work
|
|
31
|
+
});
|
|
32
|
+
let prompt = `File Path: ${filePath}\n\nContent:\n${content}`;
|
|
33
|
+
if (error) {
|
|
34
|
+
prompt += `\n\nLocal Validator Error: ${error}`;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const result = await chat.sendMessage(prompt);
|
|
38
|
+
let output = result.response.text().trim();
|
|
39
|
+
// Clean up potential markdown blocks if the model ignored the rule
|
|
40
|
+
if (output.startsWith('\`\`\`')) {
|
|
41
|
+
output = output.replace(/^\`\`\`[a-zA-Z]*\n/, '').replace(/\n\`\`\`$/, '').trim();
|
|
42
|
+
}
|
|
43
|
+
if (output === 'VALID' || output === '') {
|
|
44
|
+
return undefined; // No fix needed
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
// If the AI fails (e.g., network error), we return undefined to fall back to the main agent loop
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -115,8 +115,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
|
|
|
115
115
|
const MAX_TURNS = Infinity;
|
|
116
116
|
// Recovery thresholds for handling unexpected empty API payloads
|
|
117
117
|
let emptyRetryCount = 0;
|
|
118
|
-
const MAX_EMPTY_RETRIES =
|
|
119
|
-
const visitedToolCalls = new Set();
|
|
118
|
+
const MAX_EMPTY_RETRIES = 5;
|
|
120
119
|
// Loop while the model keeps requesting tool calls
|
|
121
120
|
while (true) {
|
|
122
121
|
turnCount++;
|
|
@@ -179,7 +178,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
|
|
|
179
178
|
const collector = getMetricCollector();
|
|
180
179
|
if (collector)
|
|
181
180
|
collector.recordToolTurn();
|
|
182
|
-
const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal
|
|
181
|
+
const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal);
|
|
183
182
|
if (execRes.aborted) {
|
|
184
183
|
return '[Generation stopped by user]';
|
|
185
184
|
}
|
|
@@ -222,7 +221,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
|
|
|
222
221
|
response = followUp.response;
|
|
223
222
|
}
|
|
224
223
|
}
|
|
225
|
-
async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal
|
|
224
|
+
async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal) {
|
|
226
225
|
const toolResponses = [];
|
|
227
226
|
for (const fc of functionCalls) {
|
|
228
227
|
await inputHandler.waitForPrompt();
|
|
@@ -232,19 +231,6 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
|
|
|
232
231
|
}
|
|
233
232
|
const toolName = fc.name;
|
|
234
233
|
const toolArgs = (fc.args ?? {});
|
|
235
|
-
const callSignature = `${toolName}:${JSON.stringify(toolArgs)}`;
|
|
236
|
-
if (visitedToolCalls.has(callSignature)) {
|
|
237
|
-
toolResponses.push({
|
|
238
|
-
functionResponse: {
|
|
239
|
-
name: toolName,
|
|
240
|
-
response: {
|
|
241
|
-
error: 'You have already made this exact tool call previously. Please review your context history or try a different action.'
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
visitedToolCalls.add(callSignature);
|
|
248
234
|
// Log the ongoing tool action to the console
|
|
249
235
|
p.log.step(formatToolCall(toolName, toolArgs));
|
|
250
236
|
// If executing a CLI/shell command, wait for approval
|
|
@@ -281,6 +267,8 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
|
|
|
281
267
|
p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
|
|
282
268
|
}
|
|
283
269
|
}
|
|
270
|
+
if (!toolResult.error && (toolName === 'write_file' || toolName === 'modify_file' || toolName === 'delete_file')) {
|
|
271
|
+
}
|
|
284
272
|
toolResponses.push({
|
|
285
273
|
functionResponse: {
|
|
286
274
|
name: toolName,
|
|
@@ -8,7 +8,8 @@ import { resolveAndValidatePath, resolveAndValidateMultiWorkspacePath } from '..
|
|
|
8
8
|
import { workspaceRegistry } from './workspaceRegistry.js';
|
|
9
9
|
import { changeLogger } from './changeLogger.js';
|
|
10
10
|
import { findBestMatch, applyMatch } from '../utils/fuzzyMatch.js';
|
|
11
|
-
import {
|
|
11
|
+
import { localValidate } from '../utils/localSyntaxValidator.js';
|
|
12
|
+
import { validateAndFixSyntax } from './agent/syntaxAgent.js';
|
|
12
13
|
import { sanitizeForCDATA } from '../utils/contextPrompts.js';
|
|
13
14
|
import { findDependencies, formatDependencyResult } from '../utils/dependencyTracer.js';
|
|
14
15
|
import { atomicWriteFile } from '../utils/atomicWrite.js';
|
|
@@ -330,8 +331,20 @@ const DEFAULT_IGNORED_DIRS = new Set([
|
|
|
330
331
|
'.cache',
|
|
331
332
|
'coverage',
|
|
332
333
|
'.turbo',
|
|
334
|
+
'.tmp',
|
|
335
|
+
'temp',
|
|
336
|
+
'tmp',
|
|
337
|
+
'.minovativemind',
|
|
338
|
+
]);
|
|
339
|
+
const DEFAULT_IGNORED_FILES = new Set([
|
|
340
|
+
'package-lock.json',
|
|
341
|
+
'yarn.lock',
|
|
342
|
+
'pnpm-lock.yaml',
|
|
343
|
+
'.DS_Store',
|
|
344
|
+
'.minovative-scratch.js',
|
|
345
|
+
'testExtractor.ts',
|
|
346
|
+
'testExtractor.js',
|
|
333
347
|
]);
|
|
334
|
-
const DEFAULT_IGNORED_FILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.DS_Store']);
|
|
335
348
|
/**
|
|
336
349
|
* Parses .gitignore and .minovativemindignore to supplement the default ignore lists.
|
|
337
350
|
*/
|
|
@@ -339,7 +352,7 @@ async function getIgnoredPaths(workspaceRoot) {
|
|
|
339
352
|
const ignoredDirs = new Set(DEFAULT_IGNORED_DIRS);
|
|
340
353
|
const ignoredFiles = new Set(DEFAULT_IGNORED_FILES);
|
|
341
354
|
const ig = ignore();
|
|
342
|
-
ig.add(Array.from(DEFAULT_IGNORED_DIRS).map(d => d + '/'));
|
|
355
|
+
ig.add(Array.from(DEFAULT_IGNORED_DIRS).map((d) => d + '/'));
|
|
343
356
|
ig.add(Array.from(DEFAULT_IGNORED_FILES));
|
|
344
357
|
const filesToRead = ['.gitignore', '.minovativemindignore'];
|
|
345
358
|
for (const ignoreFile of filesToRead) {
|
|
@@ -578,23 +591,49 @@ export async function writeFile(workspaceRoot, filePath, content) {
|
|
|
578
591
|
catch {
|
|
579
592
|
// File doesn't exist
|
|
580
593
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
594
|
+
// 1. Local validation
|
|
595
|
+
let localResult = localValidate(filePath, content);
|
|
596
|
+
let finalContent = content;
|
|
597
|
+
if (!localResult.isValid) {
|
|
598
|
+
// 2. Iterative AI retry loop
|
|
599
|
+
let attempts = 0;
|
|
600
|
+
const MAX_RETRIES = 2;
|
|
601
|
+
let success = false;
|
|
602
|
+
while (attempts < MAX_RETRIES && !success) {
|
|
603
|
+
attempts++;
|
|
604
|
+
const fixed = await validateAndFixSyntax(finalContent, filePath, localResult.error);
|
|
605
|
+
if (fixed) {
|
|
606
|
+
finalContent = fixed;
|
|
607
|
+
const recheck = localValidate(filePath, finalContent);
|
|
608
|
+
if (recheck.isValid) {
|
|
609
|
+
success = true;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Update error for next iteration
|
|
613
|
+
localResult = recheck;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!success) {
|
|
621
|
+
return {
|
|
622
|
+
output: '',
|
|
623
|
+
error: `Syntax validation failed after ${attempts} attempts: ${localResult.error || 'Unknown syntax error'}`,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
587
626
|
}
|
|
588
627
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
589
628
|
changeLogger.logChange(filePath, existingContent, existingContent !== null ? 'modify' : 'create');
|
|
590
|
-
await atomicWriteFile(absPath,
|
|
629
|
+
await atomicWriteFile(absPath, finalContent, 'utf-8');
|
|
591
630
|
return { output: `Successfully wrote to "${filePath}".` };
|
|
592
631
|
}
|
|
593
632
|
catch (err) {
|
|
594
633
|
const message = err instanceof Error ? err.message : String(err);
|
|
595
634
|
const collector = getMetricCollector();
|
|
596
635
|
if (collector)
|
|
597
|
-
collector.recordWriteFailure();
|
|
636
|
+
collector.recordWriteFailure?.();
|
|
598
637
|
return {
|
|
599
638
|
output: '',
|
|
600
639
|
error: `Failed to write file "${filePath}": ${message}`,
|
|
@@ -645,7 +684,7 @@ export async function renameFile(workspaceRoot, sourcePath, targetPath) {
|
|
|
645
684
|
}
|
|
646
685
|
}
|
|
647
686
|
export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
648
|
-
const MAX_MODIFY_RETRIES =
|
|
687
|
+
const MAX_MODIFY_RETRIES = 4;
|
|
649
688
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
650
689
|
for (let attempt = 1; attempt <= MAX_MODIFY_RETRIES; attempt++) {
|
|
651
690
|
try {
|
|
@@ -658,17 +697,17 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
658
697
|
if (!match) {
|
|
659
698
|
const collector = getMetricCollector();
|
|
660
699
|
if (collector)
|
|
661
|
-
collector.recordModifyFailure();
|
|
700
|
+
collector.recordModifyFailure?.();
|
|
662
701
|
if (attempt < MAX_MODIFY_RETRIES) {
|
|
663
702
|
// Break out of inner loop, triggering a retry in outer loop
|
|
664
703
|
modified = existing; // reset
|
|
665
704
|
break;
|
|
666
705
|
}
|
|
667
706
|
// Provide a preview of the file to help the AI self-correct
|
|
668
|
-
const preview = modified.split('
|
|
707
|
+
const preview = modified.split('\n').slice(0, 30).join('\n');
|
|
669
708
|
return {
|
|
670
709
|
output: '',
|
|
671
|
-
error: `Edit #${i + 1} failed: Search content not found in "${filePath}"
|
|
710
|
+
error: `Edit #${i + 1} failed: Search content not found in "${filePath}". Please call 'read_file' to check the exact lines of code before retrying modify_file.\n\nFile start preview:\n${preview}\n...`,
|
|
672
711
|
};
|
|
673
712
|
}
|
|
674
713
|
modified = applyMatch(modified, match, edit.replaceContent);
|
|
@@ -692,15 +731,37 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
692
731
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
693
732
|
continue;
|
|
694
733
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
734
|
+
// 1. Local validation
|
|
735
|
+
let localResult = localValidate(filePath, modified);
|
|
736
|
+
let finalModified = modified;
|
|
737
|
+
if (!localResult.isValid) {
|
|
738
|
+
// 2. Iterative AI retry loop
|
|
739
|
+
let attempts = 0;
|
|
740
|
+
const MAX_RETRIES = 2;
|
|
741
|
+
let success = false;
|
|
742
|
+
while (attempts < MAX_RETRIES && !success) {
|
|
743
|
+
attempts++;
|
|
744
|
+
const fixed = await validateAndFixSyntax(finalModified, filePath);
|
|
745
|
+
if (fixed) {
|
|
746
|
+
finalModified = fixed;
|
|
747
|
+
const recheck = localValidate(filePath, finalModified);
|
|
748
|
+
if (recheck.isValid) {
|
|
749
|
+
success = true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (!success) {
|
|
757
|
+
return {
|
|
758
|
+
output: '',
|
|
759
|
+
error: `Syntax validation failed after ${attempts} attempts: ${localResult.error || 'Unknown syntax error'}`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
701
762
|
}
|
|
702
763
|
changeLogger.logChange(filePath, existing, 'modify');
|
|
703
|
-
await atomicWriteFile(absPath,
|
|
764
|
+
await atomicWriteFile(absPath, finalModified, 'utf-8');
|
|
704
765
|
return {
|
|
705
766
|
output: `Successfully applied ${edits.length} edit(s) to "${filePath}".\\nStrategies used:\\n${strategies.join('\\n')}`,
|
|
706
767
|
};
|
|
@@ -776,27 +837,27 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
|
776
837
|
try {
|
|
777
838
|
const { stdout, stderr } = await execAsync(command, {
|
|
778
839
|
cwd: workspaceRoot,
|
|
779
|
-
timeout:
|
|
840
|
+
timeout: 120_000, // 120 secgot these ond timeout
|
|
780
841
|
maxBuffer: 1024 * 1024 * 2, // 2 MB buffer
|
|
781
842
|
signal: abortSignal,
|
|
782
843
|
});
|
|
783
844
|
let output = [stdout, stderr].filter(Boolean).join('\n');
|
|
784
845
|
// Truncate command output to prevent memory blowout from massive build logs
|
|
785
|
-
const MAX_CMD_OUTPUT =
|
|
846
|
+
const MAX_CMD_OUTPUT = 30_000;
|
|
786
847
|
if (output.length > MAX_CMD_OUTPUT) {
|
|
787
848
|
output =
|
|
788
849
|
output.substring(0, MAX_CMD_OUTPUT) +
|
|
789
|
-
`\n\n... (Output truncated: ${output.length} bytes exceeded
|
|
850
|
+
`\n\n... (Output truncated: ${output.length} bytes exceeded 30KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
|
|
790
851
|
}
|
|
791
852
|
return { output: output || '(command produced no output)' };
|
|
792
853
|
}
|
|
793
854
|
catch (err) {
|
|
794
855
|
const message = err instanceof Error ? err.message : String(err);
|
|
795
856
|
// Also truncate error output
|
|
796
|
-
const MAX_ERR_OUTPUT =
|
|
857
|
+
const MAX_ERR_OUTPUT = 30_000;
|
|
797
858
|
const truncatedMsg = message.length > MAX_ERR_OUTPUT
|
|
798
859
|
? message.substring(0, MAX_ERR_OUTPUT) +
|
|
799
|
-
`\n\n... (Error output truncated: ${message.length} bytes exceeded
|
|
860
|
+
`\n\n... (Error output truncated: ${message.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
|
|
800
861
|
: message;
|
|
801
862
|
return { output: '', error: `Command failed: ${truncatedMsg}` };
|
|
802
863
|
}
|
|
@@ -1281,7 +1342,7 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
|
|
|
1281
1342
|
if (result.error) {
|
|
1282
1343
|
const collector = getMetricCollector();
|
|
1283
1344
|
if (collector)
|
|
1284
|
-
collector.recordToolFailure(toolName);
|
|
1345
|
+
collector.recordToolFailure?.(toolName);
|
|
1285
1346
|
}
|
|
1286
1347
|
return result;
|
|
1287
1348
|
}
|
package/dist/services/agent.js
CHANGED
|
@@ -171,6 +171,7 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
171
171
|
label: '/workspaces',
|
|
172
172
|
hint: 'Manage external workspaces for cross-project development',
|
|
173
173
|
},
|
|
174
|
+
{ value: '/config-key', label: '/config-key', hint: 'BYOK (Bring Your Own Key) Configuration' },
|
|
174
175
|
{ value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
|
|
175
176
|
{ value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
|
|
176
177
|
{ value: '/revert', label: '/revert', hint: 'Undo last change' },
|
package/dist/services/ai.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Content, type FunctionCall } from '@google/generative-ai';
|
|
2
2
|
export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
|
|
3
3
|
export declare function clearModelOverrides(): void;
|
|
4
4
|
export declare function setGlobalActiveModel(model: string): void;
|