minovative-mind-cli 2.3.3 → 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.
Files changed (38) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/chat.js +2 -1
  3. package/dist/services/agent/slashCommands.js +152 -2
  4. package/dist/services/agent/toolLoop.js +5 -17
  5. package/dist/services/agent-tools.js +26 -14
  6. package/dist/services/agent.js +43 -13
  7. package/dist/services/ai.d.ts +2 -2
  8. package/dist/services/ai.js +131 -14
  9. package/dist/services/changeLogger.js +2 -2
  10. package/dist/services/chatHistoryService.d.ts +8 -0
  11. package/dist/services/chatHistoryService.js +24 -11
  12. package/dist/services/contextAgent.d.ts +1 -0
  13. package/dist/services/contextAgent.js +57 -32
  14. package/dist/services/metrics.d.ts +21 -4
  15. package/dist/services/metrics.js +18 -0
  16. package/dist/services/orchestration/investigationAgent.js +0 -32
  17. package/dist/services/orchestration/investigationCache.d.ts +25 -0
  18. package/dist/services/orchestration/investigationCache.js +135 -0
  19. package/dist/services/orchestration/investigationOrchestrator.js +5 -1
  20. package/dist/services/orchestration/messageBus.d.ts +1 -1
  21. package/dist/services/orchestration/messageBus.js +14 -14
  22. package/dist/services/orchestration/orchestrator.js +1 -1
  23. package/dist/services/orchestration/readCache.js +4 -0
  24. package/dist/services/orchestration/scopedTools.js +2 -1
  25. package/dist/services/orchestration/subAgent.js +10 -26
  26. package/dist/services/proxyClient.d.ts +8 -0
  27. package/dist/services/proxyClient.js +17 -0
  28. package/dist/services/verificationService.js +15 -6
  29. package/dist/utils/config.d.ts +4 -0
  30. package/dist/utils/config.js +8 -0
  31. package/dist/utils/credentialStore.d.ts +7 -0
  32. package/dist/utils/credentialStore.js +8 -0
  33. package/dist/utils/projectStorage.js +7 -0
  34. package/dist/utils/syntaxValidator.js +125 -21
  35. package/dist/utils/systemPrompts.d.ts +2 -2
  36. package/dist/utils/systemPrompts.js +3 -3
  37. package/oclif.manifest.json +2 -2
  38. package/package.json +1 -2
@@ -0,0 +1,135 @@
1
+ import { promises as fs, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { readCache, writeCache } from '../../utils/projectStorage.js';
5
+ import { debugLog } from '../../utils/logger.js';
6
+ const CACHE_FILE = 'investigation_cache.json';
7
+ const MAX_CACHE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
8
+ export function normalizePrompt(prompt) {
9
+ let normalized = prompt.toLowerCase().trim();
10
+ normalized = normalized.replace(/\s+/g, ' ');
11
+ const fillerWords = ['please ', 'can you ', 'could you ', 'help me ', 'i need '];
12
+ for (const word of fillerWords) {
13
+ if (normalized.startsWith(word)) {
14
+ normalized = normalized.slice(word.length).trim();
15
+ }
16
+ }
17
+ return normalized;
18
+ }
19
+ export function hashPrompt(normalized) {
20
+ return crypto.createHash('sha256').update(normalized).digest('hex');
21
+ }
22
+ export async function generateWorkspaceFingerprint(workspaceRoot, relevantFiles) {
23
+ const fileStats = [];
24
+ for (const file of relevantFiles) {
25
+ try {
26
+ const fullPath = path.join(workspaceRoot, file);
27
+ const stat = await fs.stat(fullPath);
28
+ // Use mtimeMs and size for a robust fingerprint
29
+ fileStats.push(`${file}:${stat.mtimeMs}:${stat.size}`);
30
+ }
31
+ catch (e) {
32
+ if (e.code === 'ENOENT') {
33
+ fileStats.push(`${file}:deleted`);
34
+ }
35
+ else if (e.code === 'EACCES' || e.code === 'EPERM') {
36
+ // Handle permission issues by marking as changed to force re-evaluation
37
+ fileStats.push(`${file}:inaccessible`);
38
+ }
39
+ else {
40
+ // For other errors, assume it's changed
41
+ fileStats.push(`${file}:error`);
42
+ }
43
+ }
44
+ }
45
+ fileStats.sort();
46
+ return crypto.createHash('sha256').update(fileStats.join('\n')).digest('hex');
47
+ }
48
+ export async function lookupInvestigation(workspaceRoot, userPrompt) {
49
+ const normalized = normalizePrompt(userPrompt);
50
+ const hash = hashPrompt(normalized);
51
+ const store = readCache(workspaceRoot, CACHE_FILE);
52
+ if (!store || !store.entries || !store.entries[hash]) {
53
+ return null;
54
+ }
55
+ const entry = store.entries[hash];
56
+ const currentFingerprint = await generateWorkspaceFingerprint(workspaceRoot, entry.relevantFiles);
57
+ if (entry.workspaceFingerprint !== currentFingerprint) {
58
+ debugLog(`Investigation cache: relevant files modified, invalidating cache entry`);
59
+ delete store.entries[hash];
60
+ writeCache(workspaceRoot, CACHE_FILE, store);
61
+ return null;
62
+ }
63
+ debugLog(`Investigation cache: HIT for hash ${hash}`);
64
+ const bytesSaved = JSON.stringify(entry).length;
65
+ return { entry, bytesSaved };
66
+ }
67
+ export async function saveInvestigation(workspaceRoot, userPrompt, relevantFiles, summary) {
68
+ const store = readCache(workspaceRoot, CACHE_FILE) || { entries: {} };
69
+ const normalized = normalizePrompt(userPrompt);
70
+ const hash = hashPrompt(normalized);
71
+ const fingerprint = await generateWorkspaceFingerprint(workspaceRoot, relevantFiles);
72
+ store.entries[hash] = {
73
+ promptHash: hash,
74
+ relevantFiles,
75
+ summary,
76
+ workspaceFingerprint: fingerprint,
77
+ createdAt: Date.now(),
78
+ };
79
+ // Enforce 5MB LRU limit
80
+ let storeJson = JSON.stringify(store);
81
+ while (Buffer.byteLength(storeJson, 'utf8') > MAX_CACHE_SIZE_BYTES) {
82
+ const keys = Object.keys(store.entries);
83
+ if (keys.length === 0)
84
+ break;
85
+ let oldestKey = keys[0];
86
+ let oldestTime = store.entries[oldestKey].createdAt;
87
+ for (let i = 1; i < keys.length; i++) {
88
+ if (store.entries[keys[i]].createdAt < oldestTime) {
89
+ oldestKey = keys[i];
90
+ oldestTime = store.entries[keys[i]].createdAt;
91
+ }
92
+ }
93
+ delete store.entries[oldestKey];
94
+ storeJson = JSON.stringify(store);
95
+ }
96
+ writeCache(workspaceRoot, CACHE_FILE, store);
97
+ debugLog(`💾 Investigation cached for future reuse`);
98
+ }
99
+ export async function invalidateFilesFromInvestigationCache(workspaceRoot, changedFiles) {
100
+ const store = readCache(workspaceRoot, CACHE_FILE);
101
+ if (!store || !store.entries)
102
+ return;
103
+ let invalidated = 0;
104
+ for (const hash of Object.keys(store.entries)) {
105
+ const entry = store.entries[hash];
106
+ const dependsOnChange = entry.relevantFiles.some((f) => changedFiles.includes(f));
107
+ if (dependsOnChange) {
108
+ delete store.entries[hash];
109
+ invalidated++;
110
+ }
111
+ }
112
+ if (invalidated > 0) {
113
+ writeCache(workspaceRoot, CACHE_FILE, store);
114
+ debugLog(`[DEBUG] Investigation cache: invalidated ${invalidated} entries referencing changed files`);
115
+ }
116
+ }
117
+ export function getInvestigationCacheStats(workspaceRoot) {
118
+ const store = readCache(workspaceRoot, CACHE_FILE);
119
+ const entriesCount = store?.entries ? Object.keys(store.entries).length : 0;
120
+ let sizeBytes = 0;
121
+ try {
122
+ const cachePath = path.join(workspaceRoot, '.minovativemind', CACHE_FILE);
123
+ const stat = statSync(cachePath);
124
+ sizeBytes = stat.size;
125
+ }
126
+ catch (e) {
127
+ // ignore
128
+ }
129
+ return {
130
+ entries: entriesCount,
131
+ sizeBytes,
132
+ hitCount: 0,
133
+ missCount: 0,
134
+ };
135
+ }
@@ -27,7 +27,7 @@ import { buildDependencyGraph } from '../../utils/dependencyTracer.js';
27
27
  import { debugLog } from '../../utils/logger.js';
28
28
  // ─── Constants ───────────────────────────────────────────────────────
29
29
  /** Maximum total relevant files across all agents after merge. */
30
- const MAX_TOTAL_FILES = 15;
30
+ const MAX_TOTAL_FILES = 30;
31
31
  // ─── Investigation Orchestrator ──────────────────────────────────────
32
32
  export class InvestigationOrchestrator {
33
33
  /**
@@ -106,6 +106,10 @@ export class InvestigationOrchestrator {
106
106
  `Files: ${mergedResult.relevantFiles.size} (deduped from ${totalFilesBeforeDedup}) | ` +
107
107
  `Cache hits: ${cacheStats.hitCount}\n` +
108
108
  ` Duration: ${duration}s | Tokens: ${totalTokens.toLocaleString()} ${pc.dim(`(Input: ${totalInputTokens.toLocaleString()}, Output: ${totalOutputTokens.toLocaleString()})`)}`);
109
+ if (mergedResult && mergedResult.relevantFiles.size > 0) {
110
+ const { saveInvestigation } = await import('./investigationCache.js');
111
+ await saveInvestigation(workspaceRoot, userRequest, Array.from(mergedResult.relevantFiles.keys()), mergedResult.summary);
112
+ }
109
113
  return mergedResult;
110
114
  }
111
115
  /**
@@ -86,7 +86,7 @@ export declare class MessageBus {
86
86
  private readonly persistPath;
87
87
  private readonly workspaceRoot;
88
88
  /** Maximum semantic signals any single agent can post */
89
- static readonly MAX_SIGNALS_PER_AGENT = 20;
89
+ static readonly MAX_SIGNALS_PER_AGENT = 50;
90
90
  constructor(workspaceRoot: string, conversationId: string);
91
91
  /**
92
92
  * Records a tool execution into the activity log. Called by the scoped tool
@@ -38,7 +38,7 @@ export class MessageBus {
38
38
  persistPath;
39
39
  workspaceRoot;
40
40
  /** Maximum semantic signals any single agent can post */
41
- static MAX_SIGNALS_PER_AGENT = 20;
41
+ static MAX_SIGNALS_PER_AGENT = 50;
42
42
  constructor(workspaceRoot, conversationId) {
43
43
  this.workspaceRoot = workspaceRoot;
44
44
  const orchestrationDir = path.join(workspaceRoot, '.minovativemind', 'orchestration');
@@ -64,7 +64,7 @@ export class MessageBus {
64
64
  * @returns `true` if the signal was accepted, `false` if the agent hit the cap.
65
65
  */
66
66
  postSignal(signal) {
67
- const agentSignalCount = this.signals.filter(s => s.fromAgent === signal.fromAgent).length;
67
+ const agentSignalCount = this.signals.filter((s) => s.fromAgent === signal.fromAgent).length;
68
68
  if (agentSignalCount >= MessageBus.MAX_SIGNALS_PER_AGENT) {
69
69
  debugLog(`MessageBus: Agent ${signal.fromAgent} hit signal cap (${MessageBus.MAX_SIGNALS_PER_AGENT}). ` +
70
70
  `Dropping signal of type "${signal.type}".`);
@@ -85,12 +85,8 @@ export class MessageBus {
85
85
  getUnread(agentId) {
86
86
  const actCursor = this.activityCursors.get(agentId) ?? 0;
87
87
  const sigCursor = this.signalCursors.get(agentId) ?? 0;
88
- const activities = this.activityLog
89
- .slice(actCursor)
90
- .filter(e => e.agentId !== agentId);
91
- const signals = this.signals
92
- .slice(sigCursor)
93
- .filter(s => s.fromAgent !== agentId);
88
+ const activities = this.activityLog.slice(actCursor).filter((e) => e.agentId !== agentId);
89
+ const signals = this.signals.slice(sigCursor).filter((s) => s.fromAgent !== agentId);
94
90
  // Advance cursors to current end
95
91
  this.activityCursors.set(agentId, this.activityLog.length);
96
92
  this.signalCursors.set(agentId, this.signals.length);
@@ -111,7 +107,7 @@ export class MessageBus {
111
107
  * recovery — collecting partial progress before re-dispatch).
112
108
  */
113
109
  getAgentActivity(agentId) {
114
- return this.activityLog.filter(e => e.agentId === agentId);
110
+ return this.activityLog.filter((e) => e.agentId === agentId);
115
111
  }
116
112
  /**
117
113
  * Returns the total number of activity entries and signals in the bus.
@@ -131,11 +127,13 @@ export class MessageBus {
131
127
  static formatActivityEntries(entries) {
132
128
  if (entries.length === 0)
133
129
  return '';
134
- return entries.map(e => {
130
+ return entries
131
+ .map((e) => {
135
132
  const statusIcon = e.status === 'success' ? '✓' : '✗';
136
133
  const summary = e.resultSummary ? ` | ${e.resultSummary}` : '';
137
134
  return ` ${e.agentId} | ${e.tool.padEnd(14)} → ${e.target.padEnd(40)} | ${statusIcon} ${e.action}${summary}`;
138
- }).join('\n');
135
+ })
136
+ .join('\n');
139
137
  }
140
138
  /**
141
139
  * Formats semantic signals into a readable string for agent context injection.
@@ -143,7 +141,8 @@ export class MessageBus {
143
141
  static formatSignals(signals) {
144
142
  if (signals.length === 0)
145
143
  return '';
146
- return signals.map(s => {
144
+ return signals
145
+ .map((s) => {
147
146
  const tag = s.type.toUpperCase();
148
147
  switch (s.type) {
149
148
  case 'discovery':
@@ -157,7 +156,8 @@ export class MessageBus {
157
156
  default:
158
157
  return ` [SIGNAL from ${s.fromAgent}]: ${JSON.stringify(s)}`;
159
158
  }
160
- }).join('\n');
159
+ })
160
+ .join('\n');
161
161
  }
162
162
  // ─── Persistence ─────────────────────────────────────────────────
163
163
  /**
@@ -173,7 +173,7 @@ export class MessageBus {
173
173
  signalCursors: Object.fromEntries(this.signalCursors),
174
174
  };
175
175
  // Fire-and-forget — don't block the calling agent's tool loop
176
- atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch(err => {
176
+ atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch((err) => {
177
177
  debugLog(`MessageBus: Failed to persist to disk: ${err}`);
178
178
  });
179
179
  }
@@ -260,7 +260,7 @@ export class Orchestrator {
260
260
  'DO NOT list changes by task name or separate them by agent. ' +
261
261
  'Be concise, helpful, and conclude by asking if they need any further adjustments.\n' +
262
262
  '</directives>';
263
- const synthesized = await compressTextUsingFlashLite(payloadWithContext, instruction);
263
+ const synthesized = await compressTextUsingFlashLite(payloadWithContext, instruction, undefined, true);
264
264
  finalSummary += synthesized + '\n\n';
265
265
  }
266
266
  catch (e) {
@@ -17,6 +17,7 @@
17
17
  * to prevent OOM on very large monorepos.
18
18
  */
19
19
  import { debugLog } from '../../utils/logger.js';
20
+ import { getMetricCollector } from '../metrics.js';
20
21
  // ─── Constants ───────────────────────────────────────────────────────
21
22
  /** Maximum total bytes of cached file content before LRU eviction kicks in. */
22
23
  const MAX_CACHE_BYTES = 5 * 1024 * 1024; // 5 MB
@@ -45,11 +46,14 @@ export class ReadCache {
45
46
  */
46
47
  has(filePath) {
47
48
  const found = this.cache.has(filePath);
49
+ const collector = getMetricCollector();
48
50
  if (found) {
49
51
  this.hitCount++;
52
+ collector?.recordCacheHit();
50
53
  }
51
54
  else {
52
55
  this.missCount++;
56
+ collector?.recordCacheMiss();
53
57
  }
54
58
  return found;
55
59
  }
@@ -145,7 +145,8 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
145
145
  else if (name === 'grep_search') {
146
146
  targetDesc = args.pattern;
147
147
  actionDesc = 'Searched';
148
- const matchCount = Array.isArray(result) ? result.length : 0;
148
+ const outputText = typeof result === 'object' && result?.output ? result.output : String(result || '');
149
+ const matchCount = (outputText.match(/\.\/[^:]+:\d+:/g) || []).length;
149
150
  resultSummary = `${matchCount} matches`;
150
151
  }
151
152
  return result;
@@ -63,11 +63,14 @@ export class SubAgentRunner {
63
63
  `${this.globalContext}\n` +
64
64
  `</reference_context>\n\n` +
65
65
  `<critical_guidelines>\n` +
66
- `1. You are ONE worker in a team. You MUST ONLY focus on your specific objective: "${this.intent}".\n` +
67
- `2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents are handling the other parts.\n` +
68
- `3. You are part of a parallelized system. Use 'post_message' to coordinate if you discover breaking changes.\n` +
69
- `4. When you have completed your objective, stop using tools and provide a final summary of your work.\n` +
70
- `5. If you encounter an insurmountable error, provide a summary of what went wrong so the orchestrator can re-assign or fix it.\n` +
66
+ `1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
67
+ `2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents handle other tasks.\n` +
68
+ `3. EXECUTION WORKFLOW:\n` +
69
+ ` - Step 1 (Locate): Call 'grep_search' or 'read_file' ONCE to inspect the file you need to edit.\n` +
70
+ ` - Step 2 (Modify): Call 'modify_file' or 'write_file' to implement the required changes.\n` +
71
+ ` - Step 3 (Conclude): Immediately STOP using tools and return a text summary of your changes.\n` +
72
+ `4. Do NOT call 'grep_search' or 'run_command' repeatedly in a loop. Once you have file context or command results, proceed directly to modifying code or responding with your text summary.\n` +
73
+ `5. Use 'post_message' only if you discover breaking changes affecting other agents.\n` +
71
74
  `</critical_guidelines>`);
72
75
  }
73
76
  /**
@@ -87,12 +90,10 @@ export class SubAgentRunner {
87
90
  */
88
91
  async execute(signal) {
89
92
  return runWithAgentId(this.taskId, async () => {
90
- debugLog(`SubAgent [${this.taskId}]: Starting execution.`);
91
- this.lastHeartbeat = Date.now();
92
93
  let success = false;
93
94
  let finalSummary = '';
94
95
  let crashed = false;
95
- // Health Monitor Timer
96
+ // Health monitor interval: checks every 5s if the last tool execution/heartbeat stalled
96
97
  const healthMonitor = setInterval(() => {
97
98
  if (Date.now() - this.lastHeartbeat > SubAgentRunner.STALL_TIMEOUT_MS) {
98
99
  debugLog(`SubAgent [${this.taskId}]: STALL DETECTED. No heartbeat for ${SubAgentRunner.STALL_TIMEOUT_MS}ms.`);
@@ -103,14 +104,12 @@ export class SubAgentRunner {
103
104
  }, 5000);
104
105
  try {
105
106
  // Send initial prompt
106
- const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nYou must use tools to achieve this objective. Do not stop until the objective is fully complete.`;
107
+ const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nFollow the 3-step workflow: 1) inspect file -> 2) modify code -> 3) respond with text summary. Do not repeat search tools once results are returned.`;
107
108
  let turnResult = await this.chat.sendMessage(prompt, undefined, signal);
108
109
  this.updateUsage(turnResult);
109
110
  // Tool Loop
110
111
  const MAX_TURNS = Infinity;
111
112
  let turns = 0;
112
- // Anti-loop tracking
113
- const visitedToolCalls = new Set();
114
113
  while (turns < MAX_TURNS && !crashed && !signal.aborted) {
115
114
  this.pingHeartbeat();
116
115
  const calls = turnResult.response.functionCalls();
@@ -125,21 +124,6 @@ export class SubAgentRunner {
125
124
  for (const call of calls) {
126
125
  if (crashed || signal.aborted)
127
126
  break;
128
- // Anti-loop check: Hash the call to detect exact repetitions
129
- const callSignature = `${call.name}:${JSON.stringify(call.args)}`;
130
- if (visitedToolCalls.has(callSignature)) {
131
- debugLog(`SubAgent [${this.taskId}]: Detected identical tool call ${call.name}. Blocking to prevent loop.`);
132
- toolResponses.push({
133
- functionResponse: {
134
- name: call.name,
135
- response: {
136
- error: 'You have already made this exact tool call previously. Please review your context history or try a different action.',
137
- },
138
- },
139
- });
140
- continue;
141
- }
142
- visitedToolCalls.add(callSignature);
143
127
  this.pingHeartbeat();
144
128
  if (this.onProgress) {
145
129
  this.onProgress(`executing ${call.name}...`);
@@ -32,6 +32,14 @@ export declare function peekTurnUsage(): {
32
32
  remainingBalance: number | undefined;
33
33
  modelsUsed: Record<string, number>;
34
34
  };
35
+ export declare function accumulateTurnUsage(usage: {
36
+ promptTokens?: number;
37
+ candidatesTokens?: number;
38
+ cachedTokens?: number;
39
+ promptTokenCount?: number;
40
+ candidatesTokenCount?: number;
41
+ cachedContentTokenCount?: number;
42
+ }, modelName: string): void;
35
43
  /**
36
44
  * Client service interacting directly with the serverless Gemini proxy endpoint.
37
45
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -1,4 +1,5 @@
1
1
  import { debugLog } from '../utils/logger.js';
2
+ import { getMetricCollector } from './metrics.js';
2
3
  /**
3
4
  * ============================================================================
4
5
  * PROXY CLIENT SERVICE
@@ -58,6 +59,16 @@ export function getAndResetTurnUsage() {
58
59
  export function peekTurnUsage() {
59
60
  return { ...globalSessionAccumulatedUsage };
60
61
  }
62
+ export function accumulateTurnUsage(usage, modelName) {
63
+ const pTokens = usage.promptTokens ?? usage.promptTokenCount ?? 0;
64
+ const cTokens = usage.candidatesTokens ?? usage.candidatesTokenCount ?? 0;
65
+ const cachedTokens = usage.cachedTokens ?? usage.cachedContentTokenCount ?? 0;
66
+ globalSessionAccumulatedUsage.promptTokens += pTokens;
67
+ globalSessionAccumulatedUsage.candidatesTokens += cTokens;
68
+ globalSessionAccumulatedUsage.cachedTokens += cachedTokens;
69
+ globalSessionAccumulatedUsage.totalTokenCount += pTokens + cachedTokens + cTokens;
70
+ globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
71
+ }
61
72
  /**
62
73
  * Client service interacting directly with the serverless Gemini proxy endpoint.
63
74
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -183,6 +194,12 @@ export class ProxyClient {
183
194
  else if (data.type === 'done') {
184
195
  if (data.usage) {
185
196
  usageMetadata = data.usage;
197
+ const collector = getMetricCollector();
198
+ collector?.accumulateUsage({
199
+ promptTokens: data.usage.promptTokens || 0,
200
+ candidatesTokens: data.usage.candidatesTokens || 0,
201
+ cachedTokens: data.usage.cachedTokens || 0
202
+ });
186
203
  globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
187
204
  globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
188
205
  globalSessionAccumulatedUsage.cachedTokens += data.usage.cachedTokens || 0;
@@ -11,7 +11,7 @@ export async function detectVerificationCommand(workspaceRoot) {
11
11
  { name: 'pnpm-lock.yaml', prefix: 'pnpm run' },
12
12
  { name: 'yarn.lock', prefix: 'yarn run' },
13
13
  { name: 'bun.lockb', prefix: 'bun run' },
14
- { name: 'bun.lock', prefix: 'bun run' }
14
+ { name: 'bun.lock', prefix: 'bun run' },
15
15
  ];
16
16
  for (const lf of lockFiles) {
17
17
  try {
@@ -66,14 +66,20 @@ export async function detectVerificationCommand(workspaceRoot) {
66
66
  catch { }
67
67
  try {
68
68
  await fs.access(path.join(workspaceRoot, 'build.gradle'));
69
- return (await fs.access(path.join(workspaceRoot, 'gradlew')).then(() => true).catch(() => false))
69
+ return (await fs
70
+ .access(path.join(workspaceRoot, 'gradlew'))
71
+ .then(() => true)
72
+ .catch(() => false))
70
73
  ? './gradlew classes testClasses'
71
74
  : 'gradle classes testClasses';
72
75
  }
73
76
  catch { }
74
77
  try {
75
78
  await fs.access(path.join(workspaceRoot, 'build.gradle.kts'));
76
- return (await fs.access(path.join(workspaceRoot, 'gradlew')).then(() => true).catch(() => false))
79
+ return (await fs
80
+ .access(path.join(workspaceRoot, 'gradlew'))
81
+ .then(() => true)
82
+ .catch(() => false))
77
83
  ? './gradlew classes testClasses'
78
84
  : 'gradle classes testClasses';
79
85
  }
@@ -126,7 +132,10 @@ export async function detectVerificationCommand(workspaceRoot) {
126
132
  }
127
133
  try {
128
134
  await fs.access(path.join(workspaceRoot, 'Gemfile'));
129
- return (await fs.access(path.join(workspaceRoot, 'spec')).then(() => true).catch(() => false))
135
+ return (await fs
136
+ .access(path.join(workspaceRoot, 'spec'))
137
+ .then(() => true)
138
+ .catch(() => false))
130
139
  ? 'bundle exec rspec'
131
140
  : 'bundle exec rubocop';
132
141
  }
@@ -137,7 +146,7 @@ export async function runVerification(workspaceRoot, abortSignal) {
137
146
  const command = await detectVerificationCommand(workspaceRoot);
138
147
  if (!command)
139
148
  return null;
140
- const MAX_VERIFY_OUTPUT = 20_000; // 20KB cap on verification output
149
+ const MAX_VERIFY_OUTPUT = 50_000; // 50KB cap on verification output
141
150
  debugLog(`Running project-level verification command: ${command}`);
142
151
  try {
143
152
  const { stdout, stderr } = await execAsync(command, {
@@ -225,7 +234,7 @@ ${result.errors.join('\n')}
225
234
 
226
235
  Please fix these errors using the modify_file tool.`;
227
236
  }
228
- import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile } from '../utils/performanceAuditor.js';
237
+ import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile, } from '../utils/performanceAuditor.js';
229
238
  export async function verifyChangedFiles(workspaceRoot, filePaths, abortSignal) {
230
239
  const errors = [];
231
240
  const perfAudits = [];
@@ -23,3 +23,7 @@ export declare const GEMINI_MODELS: {
23
23
  export declare const DEFAULT_MODEL: "auto";
24
24
  /** Maximum tokens the model can output per response. */
25
25
  export declare const MAX_OUTPUT_TOKENS = 60000;
26
+ /**
27
+ * Checks if BYOK is currently enabled for the user.
28
+ */
29
+ export declare function isByokEnabled(): Promise<boolean>;
@@ -23,3 +23,11 @@ export const GEMINI_MODELS = {
23
23
  export const DEFAULT_MODEL = GEMINI_MODELS.AUTO;
24
24
  /** Maximum tokens the model can output per response. */
25
25
  export const MAX_OUTPUT_TOKENS = 60_000;
26
+ /**
27
+ * Checks if BYOK is currently enabled for the user.
28
+ */
29
+ export async function isByokEnabled() {
30
+ const { loadCredentials } = await import('./credentialStore.js');
31
+ const creds = await loadCredentials();
32
+ return !!(creds.useByok && creds.geminiApiKey);
33
+ }
@@ -4,6 +4,8 @@ export interface StoredCredentials {
4
4
  idToken?: string;
5
5
  refreshToken?: string;
6
6
  idTokenExpiry?: number;
7
+ geminiApiKey?: string;
8
+ useByok?: boolean;
7
9
  }
8
10
  /**
9
11
  * Persists authentication credentials to the most secure available store.
@@ -15,6 +17,11 @@ export interface StoredCredentials {
15
17
  * 4. AES-256-GCM encrypted file with 0600 permissions
16
18
  */
17
19
  export declare function saveCredentials(data: StoredCredentials): Promise<void>;
20
+ /**
21
+ * Updates a specific credential field without reloading all existing fields.
22
+ * Useful for partial updates (e.g., toggling BYOK).
23
+ */
24
+ export declare function updateCredentialField<K extends keyof StoredCredentials>(key: K, value: StoredCredentials[K]): Promise<void>;
18
25
  /**
19
26
  * Loads authentication credentials from the secure store.
20
27
  * Returns an empty object if no credentials are found.
@@ -388,6 +388,14 @@ export async function saveCredentials(data) {
388
388
  }
389
389
  }
390
390
  }
391
+ /**
392
+ * Updates a specific credential field without reloading all existing fields.
393
+ * Useful for partial updates (e.g., toggling BYOK).
394
+ */
395
+ export async function updateCredentialField(key, value) {
396
+ const current = await loadCredentials();
397
+ await saveCredentials({ ...current, [key]: value });
398
+ }
391
399
  /**
392
400
  * Loads authentication credentials from the secure store.
393
401
  * Returns an empty object if no credentials are found.
@@ -222,6 +222,13 @@ export async function invalidateCacheForDependents(workspaceRoot, changedFiles)
222
222
  if (updated) {
223
223
  writeCache(workspaceRoot, 'context_cache.json', cachedContext);
224
224
  }
225
+ try {
226
+ const { invalidateFilesFromInvestigationCache } = await import('../services/orchestration/investigationCache.js');
227
+ await invalidateFilesFromInvestigationCache(workspaceRoot, changedFiles);
228
+ }
229
+ catch (e) {
230
+ // Ignored
231
+ }
225
232
  }
226
233
  catch (err) {
227
234
  console.warn(pc.yellow(`Failed to invalidate cache for dependents: ${err}`));