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
@@ -1,6 +1,35 @@
1
- import { readCache, writeCache } from '../utils/projectStorage.js';
1
+ import { readCache, writeCache, getProjectStorageDir } from '../utils/projectStorage.js';
2
+ import { estimateTokenCount } from '../utils/historyPrompt.js';
2
3
  /**
3
- * Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
4
+ * Estimates the token count for a Gemini Content object.
5
+ *
6
+ * @param content The Content object containing parts and role.
7
+ * @returns Estimated number of tokens.
8
+ */
9
+ export function estimateContentTokens(content) {
10
+ if (!content || !content.parts || !Array.isArray(content.parts))
11
+ return 0;
12
+ let totalChars = 0;
13
+ for (const part of content.parts) {
14
+ if ('text' in part && typeof part.text === 'string') {
15
+ totalChars += part.text.length;
16
+ }
17
+ else if ('functionCall' in part && part.functionCall) {
18
+ totalChars += JSON.stringify(part.functionCall).length;
19
+ }
20
+ else if ('functionResponse' in part && part.functionResponse) {
21
+ totalChars += JSON.stringify(part.functionResponse).length;
22
+ }
23
+ else if ('inlineData' in part && part.inlineData) {
24
+ totalChars += 100; // Minimal estimate for media payload metadata
25
+ }
26
+ }
27
+ return estimateTokenCount(totalChars > 0 ? 'x'.repeat(totalChars) : '');
28
+ }
29
+ /**
30
+ * Service responsible for managing the persistence, retrieval, history pruning,
31
+ * and deletion of chat session histories.
32
+ *
4
33
  * It stores session metadata and message history in a local JSON cache (`chat_sessions.json`)
5
34
  * within the project's storage directory and enforces a maximum limit of 250 sessions to prevent
6
35
  * unbounded storage growth.
@@ -10,6 +39,10 @@ class ChatHistoryService {
10
39
  workspaceRoot = '';
11
40
  /** The maximum number of chat sessions allowed in the cache. Oldest sessions are discarded when this limit is exceeded. */
12
41
  MAX_SESSIONS = 250;
42
+ /** Default maximum entries allowed in active history before automatic archiving. */
43
+ DEFAULT_MAX_HISTORY_ENTRIES = 60;
44
+ /** Default maximum estimated tokens before pruning. */
45
+ DEFAULT_MAX_TOKENS = 64000;
13
46
  /**
14
47
  * Initializes the chat history service with the workspace root path.
15
48
  * This must be called before attempting to read, save, or delete sessions.
@@ -31,31 +64,226 @@ class ChatHistoryService {
31
64
  const sessions = readCache(this.workspaceRoot, 'chat_sessions.json');
32
65
  return sessions || [];
33
66
  }
67
+ /**
68
+ * Retrieves a single chat session by ID.
69
+ *
70
+ * @param id - The unique session identifier.
71
+ * @returns The session data, or null if not found.
72
+ */
73
+ getSession(id) {
74
+ if (!this.workspaceRoot || !id)
75
+ return null;
76
+ const sessions = this.getSessions();
77
+ return sessions.find((s) => s.id === id) || null;
78
+ }
79
+ /**
80
+ * Estimates the total token count of all messages within a given chat session.
81
+ *
82
+ * @param session The chat session to evaluate.
83
+ * @returns Total estimated tokens.
84
+ */
85
+ estimateSessionTokens(session) {
86
+ if (!session || !session.history || !Array.isArray(session.history))
87
+ return 0;
88
+ return session.history.reduce((sum, content) => sum + estimateContentTokens(content), 0);
89
+ }
90
+ /**
91
+ * Validates and cleans conversation turns by removing empty parts or invalid structures.
92
+ *
93
+ * @param history Array of Content objects.
94
+ * @returns Sanitized array of Content objects.
95
+ */
96
+ sanitizeSessionHistory(history) {
97
+ if (!history || !Array.isArray(history))
98
+ return [];
99
+ return history.filter((content) => {
100
+ if (!content || !content.role || !Array.isArray(content.parts) || content.parts.length === 0) {
101
+ return false;
102
+ }
103
+ // Check if parts have actual non-empty content
104
+ return content.parts.some((p) => {
105
+ if ('text' in p && typeof p.text === 'string')
106
+ return p.text.trim().length > 0;
107
+ if ('functionCall' in p && p.functionCall)
108
+ return true;
109
+ if ('functionResponse' in p && p.functionResponse)
110
+ return true;
111
+ if ('inlineData' in p && p.inlineData)
112
+ return true;
113
+ return false;
114
+ });
115
+ });
116
+ }
117
+ /**
118
+ * Appends pruned history items to the session's archive JSON file on disk.
119
+ *
120
+ * @param id - The session identifier.
121
+ * @param prunedEntries - Array of pruned Content objects.
122
+ */
123
+ async archiveSessionHistory(id, prunedEntries) {
124
+ if (!this.workspaceRoot || !id || prunedEntries.length === 0)
125
+ return;
126
+ const archiveFile = `archives/archived_history_${id}.json`;
127
+ const existingArchive = readCache(this.workspaceRoot, archiveFile) || [];
128
+ await writeCache(this.workspaceRoot, archiveFile, [...existingArchive, ...prunedEntries]);
129
+ }
130
+ /**
131
+ * Retrieves previously archived history entries for a given session.
132
+ *
133
+ * @param id - The session identifier.
134
+ * @returns Array of archived Content objects.
135
+ */
136
+ async getArchivedSessionHistory(id) {
137
+ if (!this.workspaceRoot || !id)
138
+ return [];
139
+ const archiveFile = `archives/archived_history_${id}.json`;
140
+ const archive = readCache(this.workspaceRoot, archiveFile);
141
+ return archive || [];
142
+ }
143
+ /**
144
+ * Prunes a session's history to satisfy entry and token bounds while preserving
145
+ * recent conversational context and alternating turn integrity.
146
+ *
147
+ * @param session The session to prune.
148
+ * @param options History pruning configuration options.
149
+ * @returns An object containing the pruned session and count of archived entries.
150
+ */
151
+ async pruneSessionHistory(session, options) {
152
+ const maxEntries = options?.maxEntries ?? this.DEFAULT_MAX_HISTORY_ENTRIES;
153
+ const maxTokens = options?.maxTokens ?? this.DEFAULT_MAX_TOKENS;
154
+ const minRecent = options?.minRecentEntries ?? 10;
155
+ const shouldArchive = options?.archivePruned ?? true;
156
+ const sanitized = this.sanitizeSessionHistory(session.history);
157
+ let history = [...sanitized];
158
+ let archivedCount = 0;
159
+ const toArchive = [];
160
+ // Phase 1: Entry count limit pruning (prune in pairs to preserve user/model turn parity)
161
+ if (history.length > maxEntries) {
162
+ const excess = history.length - maxEntries;
163
+ const trimCount = excess % 2 === 0 ? excess : excess + 1;
164
+ const pruned = history.slice(0, trimCount);
165
+ history = history.slice(trimCount);
166
+ toArchive.push(...pruned);
167
+ archivedCount += pruned.length;
168
+ }
169
+ // Phase 2: Token budget limit pruning
170
+ let currentTokens = history.reduce((sum, c) => sum + estimateContentTokens(c), 0);
171
+ while (currentTokens > maxTokens && history.length > minRecent) {
172
+ // Remove a pair of turns (user + assistant) from the oldest end
173
+ const prunePairCount = Math.min(2, history.length - minRecent);
174
+ if (prunePairCount <= 0)
175
+ break;
176
+ const pruned = history.slice(0, prunePairCount);
177
+ history = history.slice(prunePairCount);
178
+ toArchive.push(...pruned);
179
+ archivedCount += pruned.length;
180
+ currentTokens = history.reduce((sum, c) => sum + estimateContentTokens(c), 0);
181
+ }
182
+ if (shouldArchive && toArchive.length > 0) {
183
+ await this.archiveSessionHistory(session.id, toArchive);
184
+ }
185
+ const prunedSession = {
186
+ ...session,
187
+ history,
188
+ };
189
+ return { prunedSession, archivedCount };
190
+ }
34
191
  /**
35
192
  * Saves or updates a chat session in the local workspace cache.
36
193
  * If a session with the same ID already exists, it is updated in place. Otherwise, it is appended.
194
+ * Automatically executes history pruning and archiving if the session exceeds token/turn thresholds.
37
195
  * Enforces the maximum session limit of 250 by removing the oldest session if the limit is exceeded.
38
196
  *
39
197
  * @param session - The chat session data to be saved.
198
+ * @param pruneOptions - Optional pruning configuration overrides.
40
199
  * @returns A promise that resolves when the session has been successfully written to the cache.
41
200
  */
42
- async saveSession(session) {
201
+ async saveSession(session, pruneOptions) {
43
202
  if (!this.workspaceRoot)
44
203
  return;
204
+ // Prune history if necessary
205
+ const { prunedSession } = await this.pruneSessionHistory(session, pruneOptions);
45
206
  const sessions = this.getSessions();
46
- const index = sessions.findIndex((s) => s.id === session.id);
207
+ const index = sessions.findIndex((s) => s.id === prunedSession.id);
47
208
  if (index >= 0) {
48
- sessions[index] = session;
209
+ sessions[index] = prunedSession;
49
210
  }
50
211
  else {
51
- sessions.push(session);
212
+ sessions.push(prunedSession);
52
213
  }
53
- // Keep history bounded
214
+ // Keep session list bounded to MAX_SESSIONS
54
215
  if (sessions.length > this.MAX_SESSIONS) {
55
- sessions.shift(); // Remove oldest
216
+ const removed = sessions.shift(); // Remove oldest
217
+ if (removed?.id) {
218
+ // Clean up archived history of evicted session to avoid orphan files
219
+ try {
220
+ const { promises: fs } = await import('node:fs');
221
+ const path = await import('node:path');
222
+ const storageDir = getProjectStorageDir(this.workspaceRoot);
223
+ const archivePath = path.join(storageDir, 'archives', `archived_history_${removed.id}.json`);
224
+ await fs.unlink(archivePath);
225
+ }
226
+ catch {
227
+ // Ignore if archive did not exist
228
+ }
229
+ }
56
230
  }
57
231
  await writeCache(this.workspaceRoot, 'chat_sessions.json', sessions);
58
232
  }
233
+ /**
234
+ * Retrieves a chat session with its history constrained to a specific token budget.
235
+ *
236
+ * @param id - The unique session identifier.
237
+ * @param maxTokens - Maximum token budget for the returned history.
238
+ * @returns The session with budget-constrained history, or null if session not found.
239
+ */
240
+ async getSessionWithTokenBudget(id, maxTokens) {
241
+ const session = this.getSession(id);
242
+ if (!session)
243
+ return null;
244
+ const { prunedSession } = await this.pruneSessionHistory(session, {
245
+ maxTokens,
246
+ archivePruned: false, // Reading with budget does not mutate the persistent archive
247
+ });
248
+ return prunedSession;
249
+ }
250
+ /**
251
+ * Prunes stale sessions based on age (in days) or total count limit.
252
+ *
253
+ * @param options Pruning options specifying maxAgeDays and maxTotalSessions.
254
+ * @returns Number of sessions pruned.
255
+ */
256
+ async pruneOldSessions(options) {
257
+ if (!this.workspaceRoot)
258
+ return 0;
259
+ const sessions = this.getSessions();
260
+ if (sessions.length === 0)
261
+ return 0;
262
+ const maxAgeMs = (options?.maxAgeDays ?? 90) * 24 * 60 * 60 * 1000;
263
+ const maxTotal = options?.maxTotalSessions ?? this.MAX_SESSIONS;
264
+ const now = Date.now();
265
+ const idsToDelete = [];
266
+ const retainedSessions = [];
267
+ for (const session of sessions) {
268
+ const age = now - session.timestamp;
269
+ if (age > maxAgeMs) {
270
+ idsToDelete.push(session.id);
271
+ }
272
+ else {
273
+ retainedSessions.push(session);
274
+ }
275
+ }
276
+ // If still over maxTotal, remove oldest
277
+ while (retainedSessions.length > maxTotal) {
278
+ const oldest = retainedSessions.shift();
279
+ if (oldest)
280
+ idsToDelete.push(oldest.id);
281
+ }
282
+ if (idsToDelete.length > 0) {
283
+ await this.bulkDeleteSessions(idsToDelete);
284
+ }
285
+ return idsToDelete.length;
286
+ }
59
287
  /**
60
288
  * Updates the user-friendly title of an existing chat session in the local workspace cache.
61
289
  * If the session with the matching ID exists, its title is updated and persisted to `chat_sessions.json`.
@@ -102,7 +330,6 @@ class ChatHistoryService {
102
330
  // Also delete the archived histories if they exist
103
331
  const { promises: fs } = await import('node:fs');
104
332
  const path = await import('node:path');
105
- const { getProjectStorageDir } = await import('../utils/projectStorage.js');
106
333
  const storageDir = getProjectStorageDir(this.workspaceRoot);
107
334
  await Promise.all(ids.map(async (id) => {
108
335
  try {