minovative-mind-cli 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/commands/chat.js +1 -0
- package/dist/services/agent/slashCommands.js +106 -2
- package/dist/services/agent/toolLoop.js +5 -17
- package/dist/services/agent-tools.js +26 -14
- 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 +10 -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/syntaxValidator.js +125 -21
- package/oclif.manifest.json +2 -2
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -93,6 +93,11 @@ Hot-swap during a session using `/models`:
|
|
|
93
93
|
| **Gemini 3.1 Pro** | Complex architectural changes |
|
|
94
94
|
| **Gemini 3.5 Flash Lite** | Best for speed and cost efficiency |
|
|
95
95
|
|
|
96
|
+
### BYOK (Bring Your Own Key)
|
|
97
|
+
If you prefer to use your own Gemini API key, you can configure it via the settings.
|
|
98
|
+
* **Configuration:** Set `useByok: true` and `geminiApiKey: "YOUR_KEY"` in your credentials storage.
|
|
99
|
+
* **Error Handling:** If your key is invalid, expired, or you hit rate limits, the CLI will report a `BYOK AI Error`. Please check your API key status in the Google AI Studio dashboard.
|
|
100
|
+
|
|
96
101
|
> Background tasks (routing, compression, commits) always use lightweight
|
|
97
102
|
> models automatically. You pay for those lightweight model
|
|
98
103
|
> background ai models and for your selected model
|
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
|
}
|
|
@@ -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,
|
|
@@ -330,8 +330,20 @@ const DEFAULT_IGNORED_DIRS = new Set([
|
|
|
330
330
|
'.cache',
|
|
331
331
|
'coverage',
|
|
332
332
|
'.turbo',
|
|
333
|
+
'.tmp',
|
|
334
|
+
'temp',
|
|
335
|
+
'tmp',
|
|
336
|
+
'.minovativemind',
|
|
337
|
+
]);
|
|
338
|
+
const DEFAULT_IGNORED_FILES = new Set([
|
|
339
|
+
'package-lock.json',
|
|
340
|
+
'yarn.lock',
|
|
341
|
+
'pnpm-lock.yaml',
|
|
342
|
+
'.DS_Store',
|
|
343
|
+
'.minovative-scratch.js',
|
|
344
|
+
'testExtractor.ts',
|
|
345
|
+
'testExtractor.js',
|
|
333
346
|
]);
|
|
334
|
-
const DEFAULT_IGNORED_FILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.DS_Store']);
|
|
335
347
|
/**
|
|
336
348
|
* Parses .gitignore and .minovativemindignore to supplement the default ignore lists.
|
|
337
349
|
*/
|
|
@@ -339,7 +351,7 @@ async function getIgnoredPaths(workspaceRoot) {
|
|
|
339
351
|
const ignoredDirs = new Set(DEFAULT_IGNORED_DIRS);
|
|
340
352
|
const ignoredFiles = new Set(DEFAULT_IGNORED_FILES);
|
|
341
353
|
const ig = ignore();
|
|
342
|
-
ig.add(Array.from(DEFAULT_IGNORED_DIRS).map(d => d + '/'));
|
|
354
|
+
ig.add(Array.from(DEFAULT_IGNORED_DIRS).map((d) => d + '/'));
|
|
343
355
|
ig.add(Array.from(DEFAULT_IGNORED_FILES));
|
|
344
356
|
const filesToRead = ['.gitignore', '.minovativemindignore'];
|
|
345
357
|
for (const ignoreFile of filesToRead) {
|
|
@@ -582,7 +594,7 @@ export async function writeFile(workspaceRoot, filePath, content) {
|
|
|
582
594
|
if (!validation.valid) {
|
|
583
595
|
return {
|
|
584
596
|
output: '',
|
|
585
|
-
error: `Syntax validation failed for "${filePath}"
|
|
597
|
+
error: `Syntax validation failed for "${filePath}":\n- ${validation.errors.join('\n- ')}\n\nPlease fix the syntax and try again.`,
|
|
586
598
|
};
|
|
587
599
|
}
|
|
588
600
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
@@ -594,7 +606,7 @@ export async function writeFile(workspaceRoot, filePath, content) {
|
|
|
594
606
|
const message = err instanceof Error ? err.message : String(err);
|
|
595
607
|
const collector = getMetricCollector();
|
|
596
608
|
if (collector)
|
|
597
|
-
collector.recordWriteFailure();
|
|
609
|
+
collector.recordWriteFailure?.();
|
|
598
610
|
return {
|
|
599
611
|
output: '',
|
|
600
612
|
error: `Failed to write file "${filePath}": ${message}`,
|
|
@@ -645,7 +657,7 @@ export async function renameFile(workspaceRoot, sourcePath, targetPath) {
|
|
|
645
657
|
}
|
|
646
658
|
}
|
|
647
659
|
export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
648
|
-
const MAX_MODIFY_RETRIES =
|
|
660
|
+
const MAX_MODIFY_RETRIES = 4;
|
|
649
661
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
650
662
|
for (let attempt = 1; attempt <= MAX_MODIFY_RETRIES; attempt++) {
|
|
651
663
|
try {
|
|
@@ -658,17 +670,17 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
658
670
|
if (!match) {
|
|
659
671
|
const collector = getMetricCollector();
|
|
660
672
|
if (collector)
|
|
661
|
-
collector.recordModifyFailure();
|
|
673
|
+
collector.recordModifyFailure?.();
|
|
662
674
|
if (attempt < MAX_MODIFY_RETRIES) {
|
|
663
675
|
// Break out of inner loop, triggering a retry in outer loop
|
|
664
676
|
modified = existing; // reset
|
|
665
677
|
break;
|
|
666
678
|
}
|
|
667
679
|
// Provide a preview of the file to help the AI self-correct
|
|
668
|
-
const preview = modified.split('
|
|
680
|
+
const preview = modified.split('\n').slice(0, 30).join('\n');
|
|
669
681
|
return {
|
|
670
682
|
output: '',
|
|
671
|
-
error: `Edit #${i + 1} failed: Search content not found in "${filePath}"
|
|
683
|
+
error: `Edit #${i + 1} failed: Search content not found in "${filePath}". Please call 'read_file' to check the exact lines of code before retrying modify_file.\n\nFile start preview:\n${preview}\n...`,
|
|
672
684
|
};
|
|
673
685
|
}
|
|
674
686
|
modified = applyMatch(modified, match, edit.replaceContent);
|
|
@@ -776,27 +788,27 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
|
776
788
|
try {
|
|
777
789
|
const { stdout, stderr } = await execAsync(command, {
|
|
778
790
|
cwd: workspaceRoot,
|
|
779
|
-
timeout:
|
|
791
|
+
timeout: 120_000, // 120 second timeout
|
|
780
792
|
maxBuffer: 1024 * 1024 * 2, // 2 MB buffer
|
|
781
793
|
signal: abortSignal,
|
|
782
794
|
});
|
|
783
795
|
let output = [stdout, stderr].filter(Boolean).join('\n');
|
|
784
796
|
// Truncate command output to prevent memory blowout from massive build logs
|
|
785
|
-
const MAX_CMD_OUTPUT =
|
|
797
|
+
const MAX_CMD_OUTPUT = 30_000;
|
|
786
798
|
if (output.length > MAX_CMD_OUTPUT) {
|
|
787
799
|
output =
|
|
788
800
|
output.substring(0, MAX_CMD_OUTPUT) +
|
|
789
|
-
`\n\n... (Output truncated: ${output.length} bytes exceeded
|
|
801
|
+
`\n\n... (Output truncated: ${output.length} bytes exceeded 30KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
|
|
790
802
|
}
|
|
791
803
|
return { output: output || '(command produced no output)' };
|
|
792
804
|
}
|
|
793
805
|
catch (err) {
|
|
794
806
|
const message = err instanceof Error ? err.message : String(err);
|
|
795
807
|
// Also truncate error output
|
|
796
|
-
const MAX_ERR_OUTPUT =
|
|
808
|
+
const MAX_ERR_OUTPUT = 30_000;
|
|
797
809
|
const truncatedMsg = message.length > MAX_ERR_OUTPUT
|
|
798
810
|
? message.substring(0, MAX_ERR_OUTPUT) +
|
|
799
|
-
`\n\n... (Error output truncated: ${message.length} bytes exceeded
|
|
811
|
+
`\n\n... (Error output truncated: ${message.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
|
|
800
812
|
: message;
|
|
801
813
|
return { output: '', error: `Command failed: ${truncatedMsg}` };
|
|
802
814
|
}
|
|
@@ -1281,7 +1293,7 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
|
|
|
1281
1293
|
if (result.error) {
|
|
1282
1294
|
const collector = getMetricCollector();
|
|
1283
1295
|
if (collector)
|
|
1284
|
-
collector.recordToolFailure(toolName);
|
|
1296
|
+
collector.recordToolFailure?.(toolName);
|
|
1285
1297
|
}
|
|
1286
1298
|
return result;
|
|
1287
1299
|
}
|
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;
|
package/dist/services/ai.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { GEMINI_MODELS, DEFAULT_MODEL, MAX_OUTPUT_TOKENS, isByokEnabled } from '../utils/config.js';
|
|
2
3
|
import { getToolDeclarations } from './agent-tools.js';
|
|
3
|
-
import {
|
|
4
|
+
import { getMetricCollector } from './metrics.js';
|
|
4
5
|
import { getAuthorizedIdToken } from './auth.js';
|
|
5
6
|
import { debugLog } from '../utils/logger.js';
|
|
6
7
|
import { readCache, writeCache } from '../utils/projectStorage.js';
|
|
7
8
|
import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
|
|
8
|
-
import { getMetricCollector } from './metrics.js';
|
|
9
9
|
import { workspaceRegistry } from './workspaceRegistry.js';
|
|
10
|
+
import { loadCredentials } from '../utils/credentialStore.js';
|
|
11
|
+
import { ProxyClient, accumulateTurnUsage } from './proxyClient.js';
|
|
10
12
|
function getMultiWorkspaceBlock() {
|
|
11
13
|
const summary = workspaceRegistry.buildPromptSummary();
|
|
12
14
|
if (!summary)
|
|
@@ -206,15 +208,57 @@ export class ProxyChatSession {
|
|
|
206
208
|
// Prune old history before sending to keep payload bounded
|
|
207
209
|
await this.pruneHistory();
|
|
208
210
|
const effectiveGenerationConfig = { ...this.generationConfig };
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
const byokEnabled = await isByokEnabled();
|
|
212
|
+
let result;
|
|
213
|
+
if (byokEnabled) {
|
|
214
|
+
const creds = await loadCredentials();
|
|
215
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
216
|
+
const model = genAI.getGenerativeModel({
|
|
217
|
+
model: this.modelName,
|
|
218
|
+
systemInstruction: this.systemInstruction,
|
|
219
|
+
tools: this.tools,
|
|
220
|
+
});
|
|
221
|
+
const chat = model.startChat({
|
|
222
|
+
history: this.history.slice(0, -1),
|
|
223
|
+
generationConfig: effectiveGenerationConfig,
|
|
224
|
+
});
|
|
225
|
+
const lastMessage = this.history[this.history.length - 1];
|
|
226
|
+
try {
|
|
227
|
+
const response = await chat.sendMessage(lastMessage.parts);
|
|
228
|
+
const responseObj = await response.response;
|
|
229
|
+
result = {
|
|
230
|
+
functionCalls: responseObj.functionCalls(),
|
|
231
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
232
|
+
usageMetadata: responseObj.usageMetadata,
|
|
233
|
+
};
|
|
234
|
+
if (responseObj.usageMetadata) {
|
|
235
|
+
const collector = getMetricCollector();
|
|
236
|
+
collector?.accumulateUsage(responseObj.usageMetadata);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
debugLog(`Failed to send message via BYOK: ${error}`);
|
|
241
|
+
const errorMessage = error?.message || '';
|
|
242
|
+
if (error?.status === 401 ||
|
|
243
|
+
error?.status === 403 ||
|
|
244
|
+
errorMessage.includes('API_KEY_INVALID') ||
|
|
245
|
+
errorMessage.includes('quota') ||
|
|
246
|
+
errorMessage.includes('PERMISSION_DENIED')) {
|
|
247
|
+
throw new Error('AI_BYOK_ERROR: Your API key or quota is invalid. Please run /config-key to update your settings.');
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, undefined, // toolConfig
|
|
254
|
+
this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
|
|
255
|
+
abortSignal);
|
|
256
|
+
}
|
|
212
257
|
this.latestUsageMetadata = result.usageMetadata;
|
|
213
258
|
// Track token usage metrics
|
|
214
259
|
if (result.usageMetadata) {
|
|
215
|
-
const { accumulateUsage } = await import('./metrics.js');
|
|
216
|
-
accumulateUsage(result.usageMetadata);
|
|
217
260
|
const collector = getMetricCollector();
|
|
261
|
+
collector?.accumulateUsage(result.usageMetadata);
|
|
218
262
|
if (collector) {
|
|
219
263
|
collector.recordTokenUsage(result.usageMetadata.promptTokens || 0, result.usageMetadata.candidatesTokens || 0, result.usageMetadata.cachedTokens || 0);
|
|
220
264
|
}
|
|
@@ -323,8 +367,38 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
323
367
|
parts.push({ inlineData });
|
|
324
368
|
}
|
|
325
369
|
const contents = [{ role: 'user', parts }];
|
|
326
|
-
const
|
|
327
|
-
|
|
370
|
+
const byokEnabled = await isByokEnabled();
|
|
371
|
+
let result;
|
|
372
|
+
if (byokEnabled) {
|
|
373
|
+
try {
|
|
374
|
+
const creds = await loadCredentials();
|
|
375
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
376
|
+
const modelObj = genAI.getGenerativeModel({
|
|
377
|
+
model,
|
|
378
|
+
systemInstruction: instruction,
|
|
379
|
+
});
|
|
380
|
+
const response = await modelObj.generateContent({
|
|
381
|
+
contents: [{ role: 'user', parts }],
|
|
382
|
+
generationConfig: { temperature: 0.2 },
|
|
383
|
+
});
|
|
384
|
+
const responseObj = await response.response;
|
|
385
|
+
result = {
|
|
386
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
387
|
+
usageMetadata: responseObj.usageMetadata,
|
|
388
|
+
};
|
|
389
|
+
if (responseObj.usageMetadata) {
|
|
390
|
+
accumulateTurnUsage(responseObj.usageMetadata, model);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.error('BYOK Error:', error.message);
|
|
395
|
+
throw new Error(`BYOK AI Error: ${error.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
|
|
400
|
+
undefined, instruction, { temperature: 0.2 });
|
|
401
|
+
}
|
|
328
402
|
let textPart = '';
|
|
329
403
|
if (result.parts) {
|
|
330
404
|
// Use highly optimized local loop instead of array search to avoid callback allocation and naive database regex flags
|
|
@@ -340,6 +414,9 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
340
414
|
}
|
|
341
415
|
catch (error) {
|
|
342
416
|
debugLog(`Failed to compress text using flash-lite: ${error}`);
|
|
417
|
+
if (error?.status === 401 || error?.status === 403 || error?.message?.includes('API_KEY_INVALID') || error?.message?.includes('quota')) {
|
|
418
|
+
throw new Error('AI_AUTH_ERROR: Your API key or quota is invalid. Please run /config-key or re-login.');
|
|
419
|
+
}
|
|
343
420
|
return text; // fallback to raw text if compression fails
|
|
344
421
|
}
|
|
345
422
|
}
|
|
@@ -583,8 +660,38 @@ export async function generateChatTitle(firstMessage) {
|
|
|
583
660
|
let model = getGlobalActiveModel();
|
|
584
661
|
if (model === 'auto')
|
|
585
662
|
model = GEMINI_MODELS.FLASH_LITE;
|
|
586
|
-
const
|
|
587
|
-
|
|
663
|
+
const byokEnabled = await isByokEnabled();
|
|
664
|
+
let result;
|
|
665
|
+
if (byokEnabled) {
|
|
666
|
+
try {
|
|
667
|
+
const creds = await loadCredentials();
|
|
668
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
669
|
+
const modelObj = genAI.getGenerativeModel({
|
|
670
|
+
model,
|
|
671
|
+
systemInstruction: instruction,
|
|
672
|
+
});
|
|
673
|
+
const response = await modelObj.generateContent({
|
|
674
|
+
contents,
|
|
675
|
+
generationConfig: { temperature: 0.2 },
|
|
676
|
+
});
|
|
677
|
+
const responseObj = await response.response;
|
|
678
|
+
result = {
|
|
679
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
680
|
+
usageMetadata: responseObj.usageMetadata,
|
|
681
|
+
};
|
|
682
|
+
if (responseObj.usageMetadata) {
|
|
683
|
+
accumulateTurnUsage(responseObj.usageMetadata, model);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('BYOK Error:', error.message);
|
|
688
|
+
throw new Error(`BYOK AI Error: ${error.message}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
|
|
693
|
+
undefined, instruction, { temperature: 0.2 });
|
|
694
|
+
}
|
|
588
695
|
let title = '';
|
|
589
696
|
if (result.parts) {
|
|
590
697
|
for (const p of result.parts) {
|
|
@@ -599,7 +706,15 @@ export async function generateChatTitle(firstMessage) {
|
|
|
599
706
|
return title ? title : firstMessage.substring(0, maxLength);
|
|
600
707
|
}
|
|
601
708
|
catch (error) {
|
|
602
|
-
debugLog(`Failed to generate chat title: ${error}`);
|
|
709
|
+
debugLog(`Failed to generate chat title via BYOK: ${error}`);
|
|
710
|
+
const errorMessage = error?.message || '';
|
|
711
|
+
if (error?.status === 401 ||
|
|
712
|
+
error?.status === 403 ||
|
|
713
|
+
errorMessage.includes('API_KEY_INVALID') ||
|
|
714
|
+
errorMessage.includes('quota') ||
|
|
715
|
+
errorMessage.includes('PERMISSION_DENIED')) {
|
|
716
|
+
throw new Error('AI_BYOK_ERROR: Your API key or quota is invalid. Please run /config-key to update your settings.');
|
|
717
|
+
}
|
|
603
718
|
return firstMessage.substring(0, maxLength);
|
|
604
719
|
}
|
|
605
720
|
}
|
|
@@ -21,7 +21,7 @@ class ChangeLogger {
|
|
|
21
21
|
* Maximum number of historical changesets retained in memory and on disk.
|
|
22
22
|
* Older changesets are discarded on save once this limit is exceeded.
|
|
23
23
|
*/
|
|
24
|
-
MAX_HISTORY =
|
|
24
|
+
MAX_HISTORY = 20;
|
|
25
25
|
/**
|
|
26
26
|
* Whether the change logger is currently enabled.
|
|
27
27
|
*/
|
|
@@ -66,7 +66,7 @@ class ChangeLogger {
|
|
|
66
66
|
}
|
|
67
67
|
writeCache(this.workspaceRoot, 'revert_state.json', {
|
|
68
68
|
history: this.changeStack,
|
|
69
|
-
isEnabled: this.isEnabled
|
|
69
|
+
isEnabled: this.isEnabled,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
@@ -222,7 +222,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
222
222
|
cachedFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
-
const MAX_TOTAL_FILES =
|
|
225
|
+
const MAX_TOTAL_FILES = 30;
|
|
226
226
|
try {
|
|
227
227
|
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
228
228
|
const autoDiscovered = new Set();
|
|
@@ -255,10 +255,10 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
255
255
|
projectType,
|
|
256
256
|
summary: cacheHit.entry.summary,
|
|
257
257
|
relevantFiles: cachedFiles,
|
|
258
|
-
fromMemoryBank: true
|
|
258
|
+
fromMemoryBank: true,
|
|
259
259
|
},
|
|
260
260
|
targetAgent,
|
|
261
|
-
chainedMessages: []
|
|
261
|
+
chainedMessages: [],
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
// ─── Parallel Investigation Gate ─────────────────────────────
|
|
@@ -282,9 +282,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
282
282
|
const relevantFiles = new Map();
|
|
283
283
|
let summary = 'No relevant context found.';
|
|
284
284
|
let isInvestigationFinished = false;
|
|
285
|
-
const visitedToolCalls = new Set();
|
|
286
|
-
let consecutiveDuplicates = 0;
|
|
287
|
-
const MAX_CONSECUTIVE_DUPLICATES = 3;
|
|
288
285
|
let chainedMessages = [];
|
|
289
286
|
let webSearchSummary = '';
|
|
290
287
|
// Initial prompt
|
|
@@ -336,33 +333,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
336
333
|
if (abortSignal.aborted)
|
|
337
334
|
break;
|
|
338
335
|
const args = call.args;
|
|
339
|
-
// Deduplicate identical tool calls
|
|
340
|
-
const callSignature = `${call.name}:${JSON.stringify(args)}`;
|
|
341
|
-
if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
|
|
342
|
-
consecutiveDuplicates++;
|
|
343
|
-
if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
|
|
344
|
-
// Force-finish: the model is stuck in a loop, finalize with whatever context we have
|
|
345
|
-
const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
|
|
346
|
-
debugLog(`[Context Agent] ${forceMsg}`);
|
|
347
|
-
if (onProgress)
|
|
348
|
-
onProgress(forceMsg);
|
|
349
|
-
if (!summary || summary === 'No relevant context found.') {
|
|
350
|
-
summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
|
|
351
|
-
}
|
|
352
|
-
isFinished = true;
|
|
353
|
-
isInvestigationFinished = relevantFiles.size > 0;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
functionResponses.push({
|
|
357
|
-
functionResponse: {
|
|
358
|
-
name: call.name,
|
|
359
|
-
response: { error: `DUPLICATE CALL BLOCKED (attempt ${consecutiveDuplicates}/${MAX_CONSECUTIVE_DUPLICATES}): You already executed this exact tool call. Do NOT retry it. Use the results you already have and call finish_investigation now, or try a DIFFERENT tool call with different parameters.` }
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
consecutiveDuplicates = 0;
|
|
365
|
-
visitedToolCalls.add(callSignature);
|
|
366
336
|
let logMsg = ` [Context Agent] Executing ${call.name}`;
|
|
367
337
|
if (call.name === 'finish_investigation') {
|
|
368
338
|
const filesToRead = args.relevantFiles || [];
|
|
@@ -420,7 +390,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
420
390
|
// When the Context Agent finalizes its investigation, we automatically
|
|
421
391
|
// discover files that DEPEND ON the selected files. This ensures the
|
|
422
392
|
// Execution Agent won't break imports when modifying/deleting/renaming.
|
|
423
|
-
const MAX_TOTAL_FILES =
|
|
393
|
+
const MAX_TOTAL_FILES = 30;
|
|
424
394
|
try {
|
|
425
395
|
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
426
396
|
const autoDiscovered = new Set();
|
|
@@ -599,7 +569,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
599
569
|
if (collector) {
|
|
600
570
|
collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
|
|
601
571
|
if (!isInvestigationFinished || relevantFiles.size === 0) {
|
|
602
|
-
collector
|
|
572
|
+
collector?.recordInvestigationFailure?.();
|
|
603
573
|
}
|
|
604
574
|
}
|
|
605
575
|
if (isInvestigationFinished && relevantFiles.size > 0) {
|