minovative-mind-cli 2.9.1 → 2.11.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 (34) hide show
  1. package/README.md +14 -6
  2. package/dist/services/agent/commandApproval.js +5 -2
  3. package/dist/services/agent/slashCommands.js +90 -53
  4. package/dist/services/agent-tools.d.ts +4 -3
  5. package/dist/services/agent-tools.js +32 -79
  6. package/dist/services/agent.d.ts +5 -6
  7. package/dist/services/agent.js +11 -15
  8. package/dist/services/ai.d.ts +21 -1
  9. package/dist/services/ai.js +236 -11
  10. package/dist/services/chatHistoryService.d.ts +95 -2
  11. package/dist/services/chatHistoryService.js +236 -9
  12. package/dist/services/contextAgent.js +196 -81
  13. package/dist/services/investigationComplexity.d.ts +1 -1
  14. package/dist/services/investigationComplexity.js +1 -1
  15. package/dist/services/orchestration/investigationAgent.js +101 -84
  16. package/dist/services/orchestration/investigationCache.d.ts +80 -5
  17. package/dist/services/orchestration/investigationCache.js +570 -41
  18. package/dist/services/orchestration/investigationOrchestrator.js +17 -4
  19. package/dist/services/orchestration/orchestrator.js +6 -3
  20. package/dist/services/orchestration/scopedTools.js +5 -0
  21. package/dist/services/orchestration/subAgent.d.ts +31 -1
  22. package/dist/services/orchestration/subAgent.js +153 -2
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +20 -4
  26. package/dist/utils/contextPrompts.js +158 -23
  27. package/dist/utils/historyPrompt.d.ts +92 -1
  28. package/dist/utils/historyPrompt.js +166 -2
  29. package/dist/utils/symbolExtractor.d.ts +12 -0
  30. package/dist/utils/symbolExtractor.js +946 -0
  31. package/dist/utils/systemPrompts.d.ts +5 -4
  32. package/dist/utils/systemPrompts.js +46 -14
  33. package/oclif.manifest.json +1 -1
  34. package/package.json +2 -2
@@ -5,7 +5,7 @@ import { getMetricCollector } from './metrics.js';
5
5
  import { getAuthorizedIdToken, checkByokSubscription } from './auth.js';
6
6
  import { debugLog } from '../utils/logger.js';
7
7
  import { readCache, writeCache } from '../utils/projectStorage.js';
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, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
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, INVESTIGATION_SEMANTIC_SYSTEM_INSTRUCTION, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
9
9
  import { workspaceRegistry } from './workspaceRegistry.js';
10
10
  import { loadCredentials } from '../utils/credentialStore.js';
11
11
  import { ProxyClient } from './proxyClient.js';
@@ -62,6 +62,14 @@ const MAX_HISTORY_ENTRIES = 500;
62
62
  * payload stays within sane memory bounds.
63
63
  */
64
64
  const MAX_PART_TEXT_LENGTH = 60_000;
65
+ const HISTORICAL_TOOL_OUTPUT_THRESHOLD = 1500;
66
+ const COLLAPSED_TOOL_OUTPUT_MARKER = '\n... [Historical tool output collapsed to save context]';
67
+ function collapseHistoricalOutput(val, threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD) {
68
+ if (val.length <= threshold || val.includes('[Historical tool output collapsed')) {
69
+ return val;
70
+ }
71
+ return `${val.substring(0, threshold)}${COLLAPSED_TOOL_OUTPUT_MARKER}`;
72
+ }
65
73
  function truncatePartText(text) {
66
74
  if (text.length <= MAX_PART_TEXT_LENGTH)
67
75
  return text;
@@ -117,6 +125,64 @@ export class ProxyChatSession {
117
125
  this.fullHistory.push(JSON.parse(JSON.stringify(userContent)));
118
126
  this.fullHistory.push(JSON.parse(JSON.stringify(modelContent)));
119
127
  }
128
+ /**
129
+ * Appends or merges the final conversational summary text into the active session history.
130
+ * Ensures that the final response produced by finish_task, PM reconciliation, or final turns
131
+ * is permanently preserved as a model text response in both working history and full transcript.
132
+ */
133
+ appendFinalSummary(text) {
134
+ if (!text || !text.trim())
135
+ return;
136
+ const clean = text.replace(/\[TASK_FINISHED\]/g, '').trim();
137
+ if (!clean)
138
+ return;
139
+ // 1. Ensure fullHistory has the model text response
140
+ if (this.fullHistory.length > 0) {
141
+ const lastFull = this.fullHistory[this.fullHistory.length - 1];
142
+ if (lastFull.role === 'model') {
143
+ const hasText = lastFull.parts?.some((p) => p.text && p.text.trim() === clean);
144
+ if (!hasText) {
145
+ lastFull.parts = lastFull.parts || [];
146
+ lastFull.parts.push({ text: clean });
147
+ }
148
+ }
149
+ else {
150
+ this.fullHistory.push({
151
+ role: 'model',
152
+ parts: [{ text: clean }],
153
+ });
154
+ }
155
+ }
156
+ else {
157
+ this.fullHistory.push({
158
+ role: 'model',
159
+ parts: [{ text: clean }],
160
+ });
161
+ }
162
+ // 2. Ensure working history has the model text response
163
+ if (this.history.length > 0) {
164
+ const lastHist = this.history[this.history.length - 1];
165
+ if (lastHist.role === 'model') {
166
+ const hasText = lastHist.parts?.some((p) => p.text && p.text.trim() === clean);
167
+ if (!hasText) {
168
+ lastHist.parts = lastHist.parts || [];
169
+ lastHist.parts.push({ text: clean });
170
+ }
171
+ }
172
+ else {
173
+ this.history.push({
174
+ role: 'model',
175
+ parts: [{ text: clean }],
176
+ });
177
+ }
178
+ }
179
+ else {
180
+ this.history.push({
181
+ role: 'model',
182
+ parts: [{ text: clean }],
183
+ });
184
+ }
185
+ }
120
186
  /**
121
187
  * Retrieves the most recent conversation history as a formatted string.
122
188
  * Useful for passing conversation context to stateless background agents.
@@ -164,6 +230,7 @@ export class ProxyChatSession {
164
230
  * conversational context while preventing OOM crashes.
165
231
  */
166
232
  async pruneHistory() {
233
+ this.pruneToolOutputHistory();
167
234
  if (this.history.length > MAX_HISTORY_ENTRIES) {
168
235
  const excess = this.history.length - MAX_HISTORY_ENTRIES;
169
236
  const trimCount = excess % 2 === 0 ? excess : excess + 1;
@@ -181,6 +248,46 @@ export class ProxyChatSession {
181
248
  }
182
249
  }
183
250
  }
251
+ /**
252
+ * Collapses oversized functionResponse outputs in historical turns (>1 turn old)
253
+ * while strictly maintaining Gemini functionCall and functionResponse pairing.
254
+ *
255
+ * Gemini requires every functionCall in a model turn to have a matching functionResponse
256
+ * in the immediately following user turn. Deleting parts or entries breaks this invariant and
257
+ * causes 400 Bad Request errors. This method mutates oversized response outputs in-place
258
+ * on older turns to drastically conserve context window tokens.
259
+ *
260
+ * @param threshold Maximum characters allowed for a historical tool response before collapsing (default: 1500).
261
+ * @param turnsToKeep Number of recent turns to preserve unpruned (default: 1).
262
+ */
263
+ pruneToolOutputHistory(threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD, turnsToKeep = 1) {
264
+ // 1 turn = 1 user Content + 1 model Content pair (2 entries)
265
+ const cutoffIndex = Math.max(0, this.history.length - turnsToKeep * 2);
266
+ if (cutoffIndex <= 0)
267
+ return;
268
+ for (let i = 0; i < cutoffIndex; i++) {
269
+ const entry = this.history[i];
270
+ if (entry && entry.role === 'user' && Array.isArray(entry.parts)) {
271
+ for (const part of entry.parts) {
272
+ if (part && typeof part === 'object' && 'functionResponse' in part && part.functionResponse) {
273
+ const funcResp = part.functionResponse;
274
+ if (funcResp.response && typeof funcResp.response === 'object') {
275
+ const respObj = funcResp.response;
276
+ if (typeof respObj.output === 'string') {
277
+ respObj.output = collapseHistoricalOutput(respObj.output, threshold);
278
+ }
279
+ if (typeof respObj.error === 'string') {
280
+ respObj.error = collapseHistoricalOutput(respObj.error, threshold);
281
+ }
282
+ if (typeof respObj.result === 'string') {
283
+ respObj.result = collapseHistoricalOutput(respObj.result, threshold);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
184
291
  async sendMessage(message, additionalText, abortSignal, onChunk) {
185
292
  const idToken = await getAuthorizedIdToken();
186
293
  if (!idToken) {
@@ -345,7 +452,7 @@ export function getPlanModeConfig() {
345
452
  };
346
453
  }
347
454
  /**
348
- * Compresses a large string of text using gemini-3.5-flash-lite.
455
+ * Compresses a large string of text using Gemini Flash.
349
456
  * Used for shrinking context payloads to prevent OOM/choking.
350
457
  */
351
458
  export async function compressTextUsingFlashLite(text, instruction = "<directives>\nSummarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Make sure it's understandable without the fluff.\n</directives>", inlineData, force = false, abortSignal) {
@@ -357,7 +464,7 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
357
464
  const idToken = await getAuthorizedIdToken();
358
465
  if (!idToken)
359
466
  return text;
360
- let model = GEMINI_MODELS.FLASH_LITE;
467
+ let model = GEMINI_MODELS.FLASH;
361
468
  const parts = [{ text }];
362
469
  if (inlineData) {
363
470
  parts.push({ inlineData });
@@ -397,7 +504,7 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
397
504
  if (abortSignal?.aborted || error?.name === 'AbortError' || error?.message?.includes('abort')) {
398
505
  return text;
399
506
  }
400
- debugLog(`Failed to compress text using flash-lite: ${error}`);
507
+ debugLog(`Failed to compress text using flash: ${error}`);
401
508
  if (error?.status === 401 ||
402
509
  error?.status === 403 ||
403
510
  error?.message?.includes('API_KEY_INVALID') ||
@@ -568,25 +675,143 @@ export function createContextAgentSession() {
568
675
  export function createIntentRouterSession() {
569
676
  let model = getGlobalActiveModel();
570
677
  if (model === 'auto' || model.includes('claude'))
571
- model = GEMINI_MODELS.FLASH_LITE;
678
+ model = GEMINI_MODELS.FLASH;
572
679
  return new ProxyChatSession(model, INTENT_ROUTER_SYSTEM_INSTRUCTION, [], // no tools
573
- { temperature: 0, responseMimeType: 'application/json' });
680
+ {
681
+ temperature: 0,
682
+ responseMimeType: 'application/json',
683
+ responseSchema: {
684
+ type: SchemaType.OBJECT,
685
+ properties: {
686
+ context: {
687
+ type: SchemaType.STRING,
688
+ enum: ['SEARCH', 'SKIP'],
689
+ description: 'Whether workspace context search is required',
690
+ },
691
+ agent: {
692
+ type: SchemaType.STRING,
693
+ enum: ['CHAT', 'EXECUTE'],
694
+ description: 'Target agent to route the request to',
695
+ },
696
+ },
697
+ required: ['context', 'agent'],
698
+ },
699
+ });
574
700
  }
575
701
  // ─── Execution Complexity Router Service ──────────────────────────────
576
702
  export function createExecutionComplexitySession() {
577
703
  let model = getGlobalActiveModel();
578
704
  if (model === 'auto' || model.includes('claude'))
579
- model = GEMINI_MODELS.FLASH_LITE;
705
+ model = GEMINI_MODELS.FLASH;
580
706
  return new ProxyChatSession(model, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
581
- { temperature: 0, responseMimeType: 'application/json' });
707
+ {
708
+ temperature: 0,
709
+ responseMimeType: 'application/json',
710
+ responseSchema: {
711
+ type: SchemaType.OBJECT,
712
+ properties: {
713
+ complexity: {
714
+ type: SchemaType.STRING,
715
+ enum: ['EASY', 'HARD'],
716
+ description: 'Execution task complexity rating',
717
+ },
718
+ },
719
+ required: ['complexity'],
720
+ },
721
+ });
582
722
  }
583
723
  // ─── Investigation Complexity Router Service ─────────────────────────
584
724
  export function createInvestigationComplexitySession() {
585
725
  let model = getGlobalActiveModel();
586
726
  if (model === 'auto' || model.includes('claude'))
587
- model = GEMINI_MODELS.FLASH_LITE;
727
+ model = GEMINI_MODELS.FLASH;
588
728
  return new ProxyChatSession(model, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
589
- { temperature: 0, responseMimeType: 'application/json' });
729
+ {
730
+ temperature: 0,
731
+ responseMimeType: 'application/json',
732
+ responseSchema: {
733
+ type: SchemaType.OBJECT,
734
+ properties: {
735
+ strategy: {
736
+ type: SchemaType.STRING,
737
+ enum: ['SINGLE', 'PARALLEL'],
738
+ description: 'Investigation strategy (SINGLE or PARALLEL)',
739
+ },
740
+ domains: {
741
+ type: SchemaType.ARRAY,
742
+ items: {
743
+ type: SchemaType.STRING,
744
+ },
745
+ description: 'All identified investigation domains',
746
+ },
747
+ agentAssignments: {
748
+ type: SchemaType.ARRAY,
749
+ items: {
750
+ type: SchemaType.OBJECT,
751
+ properties: {
752
+ agentLabel: {
753
+ type: SchemaType.STRING,
754
+ description: 'Label of the sub-agent',
755
+ },
756
+ domains: {
757
+ type: SchemaType.ARRAY,
758
+ items: {
759
+ type: SchemaType.STRING,
760
+ },
761
+ description: 'Domains assigned to this sub-agent',
762
+ },
763
+ },
764
+ required: ['agentLabel', 'domains'],
765
+ },
766
+ description: 'Sub-agent domain group assignments',
767
+ },
768
+ reasoning: {
769
+ type: SchemaType.STRING,
770
+ description: 'Brief justification for the chosen strategy and grouping',
771
+ },
772
+ },
773
+ required: ['strategy', 'domains', 'agentAssignments', 'reasoning'],
774
+ },
775
+ });
776
+ }
777
+ // ─── Investigation Semantic Router Service ───────────────────────────
778
+ export function createInvestigationSemanticSession() {
779
+ let model = getGlobalActiveModel();
780
+ if (model === 'auto' || model.includes('claude'))
781
+ model = GEMINI_MODELS.FLASH_LITE;
782
+ return new ProxyChatSession(model, INVESTIGATION_SEMANTIC_SYSTEM_INSTRUCTION, [], // no tools
783
+ {
784
+ temperature: 0,
785
+ responseMimeType: 'application/json',
786
+ responseSchema: {
787
+ type: SchemaType.OBJECT,
788
+ properties: {
789
+ topics: {
790
+ type: SchemaType.ARRAY,
791
+ items: {
792
+ type: SchemaType.STRING,
793
+ },
794
+ description: 'Technical topic or domain identifiers',
795
+ },
796
+ components: {
797
+ type: SchemaType.ARRAY,
798
+ items: {
799
+ type: SchemaType.STRING,
800
+ },
801
+ description: 'Target component, file, function, or endpoint names',
802
+ },
803
+ intent: {
804
+ type: SchemaType.STRING,
805
+ description: 'Primary intent category (e.g. bug_fix, feature_addition, explanation, refactoring)',
806
+ },
807
+ reasoning: {
808
+ type: SchemaType.STRING,
809
+ description: 'Brief justification for the extracted metadata',
810
+ },
811
+ },
812
+ required: ['topics', 'components', 'intent'],
813
+ },
814
+ });
590
815
  }
591
816
  // ─── Web Search Agent Service ───────────────────────────────────────────
592
817
  export function createWebSearchAgentSession() {
@@ -604,7 +829,7 @@ export function createWebSearchAgentSession() {
604
829
  export function createHistorySummarizerSession() {
605
830
  let model = getGlobalActiveModel();
606
831
  if (model === 'auto' || model.includes('claude'))
607
- model = GEMINI_MODELS.FLASH_LITE;
832
+ model = GEMINI_MODELS.FLASH;
608
833
  return new ProxyChatSession(model, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, [], {
609
834
  temperature: 0.2,
610
835
  maxOutputTokens: MAX_OUTPUT_TOKENS,
@@ -1,4 +1,17 @@
1
1
  import type { Content } from '@google/generative-ai';
2
+ /**
3
+ * Options for configuring dynamic history pruning.
4
+ */
5
+ export interface HistoryPruneOptions {
6
+ /** Maximum number of history entries (user + model contents) to keep in active memory. Defaults to 50. */
7
+ maxEntries?: number;
8
+ /** Maximum estimated tokens allowed for the active conversation history. Defaults to 64,000. */
9
+ maxTokens?: number;
10
+ /** Minimum number of recent entries to retain regardless of token limits. Defaults to 10 (5 full turns). */
11
+ minRecentEntries?: number;
12
+ /** Whether to automatically archive pruned entries to disk. Defaults to true. */
13
+ archivePruned?: boolean;
14
+ }
2
15
  /**
3
16
  * Represents the structure of a saved chat session.
4
17
  * This data is persisted in the local workspace cache to allow users to resume
@@ -41,7 +54,16 @@ export interface ChatSessionData {
41
54
  modelUsageCounts?: Record<string, number>;
42
55
  }
43
56
  /**
44
- * Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
57
+ * Estimates the token count for a Gemini Content object.
58
+ *
59
+ * @param content The Content object containing parts and role.
60
+ * @returns Estimated number of tokens.
61
+ */
62
+ export declare function estimateContentTokens(content: Content): number;
63
+ /**
64
+ * Service responsible for managing the persistence, retrieval, history pruning,
65
+ * and deletion of chat session histories.
66
+ *
45
67
  * It stores session metadata and message history in a local JSON cache (`chat_sessions.json`)
46
68
  * within the project's storage directory and enforces a maximum limit of 250 sessions to prevent
47
69
  * unbounded storage growth.
@@ -51,6 +73,10 @@ declare class ChatHistoryService {
51
73
  private workspaceRoot;
52
74
  /** The maximum number of chat sessions allowed in the cache. Oldest sessions are discarded when this limit is exceeded. */
53
75
  private readonly MAX_SESSIONS;
76
+ /** Default maximum entries allowed in active history before automatic archiving. */
77
+ private readonly DEFAULT_MAX_HISTORY_ENTRIES;
78
+ /** Default maximum estimated tokens before pruning. */
79
+ private readonly DEFAULT_MAX_TOKENS;
54
80
  /**
55
81
  * Initializes the chat history service with the workspace root path.
56
82
  * This must be called before attempting to read, save, or delete sessions.
@@ -65,15 +91,82 @@ declare class ChatHistoryService {
65
91
  * @returns An array of saved `ChatSessionData` objects, ordered as stored in the cache.
66
92
  */
67
93
  getSessions(): ChatSessionData[];
94
+ /**
95
+ * Retrieves a single chat session by ID.
96
+ *
97
+ * @param id - The unique session identifier.
98
+ * @returns The session data, or null if not found.
99
+ */
100
+ getSession(id: string): ChatSessionData | null;
101
+ /**
102
+ * Estimates the total token count of all messages within a given chat session.
103
+ *
104
+ * @param session The chat session to evaluate.
105
+ * @returns Total estimated tokens.
106
+ */
107
+ estimateSessionTokens(session: ChatSessionData): number;
108
+ /**
109
+ * Validates and cleans conversation turns by removing empty parts or invalid structures.
110
+ *
111
+ * @param history Array of Content objects.
112
+ * @returns Sanitized array of Content objects.
113
+ */
114
+ sanitizeSessionHistory(history: Content[]): Content[];
115
+ /**
116
+ * Appends pruned history items to the session's archive JSON file on disk.
117
+ *
118
+ * @param id - The session identifier.
119
+ * @param prunedEntries - Array of pruned Content objects.
120
+ */
121
+ archiveSessionHistory(id: string, prunedEntries: Content[]): Promise<void>;
122
+ /**
123
+ * Retrieves previously archived history entries for a given session.
124
+ *
125
+ * @param id - The session identifier.
126
+ * @returns Array of archived Content objects.
127
+ */
128
+ getArchivedSessionHistory(id: string): Promise<Content[]>;
129
+ /**
130
+ * Prunes a session's history to satisfy entry and token bounds while preserving
131
+ * recent conversational context and alternating turn integrity.
132
+ *
133
+ * @param session The session to prune.
134
+ * @param options History pruning configuration options.
135
+ * @returns An object containing the pruned session and count of archived entries.
136
+ */
137
+ pruneSessionHistory(session: ChatSessionData, options?: HistoryPruneOptions): Promise<{
138
+ prunedSession: ChatSessionData;
139
+ archivedCount: number;
140
+ }>;
68
141
  /**
69
142
  * Saves or updates a chat session in the local workspace cache.
70
143
  * If a session with the same ID already exists, it is updated in place. Otherwise, it is appended.
144
+ * Automatically executes history pruning and archiving if the session exceeds token/turn thresholds.
71
145
  * Enforces the maximum session limit of 250 by removing the oldest session if the limit is exceeded.
72
146
  *
73
147
  * @param session - The chat session data to be saved.
148
+ * @param pruneOptions - Optional pruning configuration overrides.
74
149
  * @returns A promise that resolves when the session has been successfully written to the cache.
75
150
  */
76
- saveSession(session: ChatSessionData): Promise<void>;
151
+ saveSession(session: ChatSessionData, pruneOptions?: HistoryPruneOptions): Promise<void>;
152
+ /**
153
+ * Retrieves a chat session with its history constrained to a specific token budget.
154
+ *
155
+ * @param id - The unique session identifier.
156
+ * @param maxTokens - Maximum token budget for the returned history.
157
+ * @returns The session with budget-constrained history, or null if session not found.
158
+ */
159
+ getSessionWithTokenBudget(id: string, maxTokens: number): Promise<ChatSessionData | null>;
160
+ /**
161
+ * Prunes stale sessions based on age (in days) or total count limit.
162
+ *
163
+ * @param options Pruning options specifying maxAgeDays and maxTotalSessions.
164
+ * @returns Number of sessions pruned.
165
+ */
166
+ pruneOldSessions(options?: {
167
+ maxAgeDays?: number;
168
+ maxTotalSessions?: number;
169
+ }): Promise<number>;
77
170
  /**
78
171
  * Updates the user-friendly title of an existing chat session in the local workspace cache.
79
172
  * If the session with the matching ID exists, its title is updated and persisted to `chat_sessions.json`.