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.
- package/README.md +14 -6
- package/dist/services/agent/commandApproval.js +5 -2
- package/dist/services/agent/slashCommands.js +90 -53
- package/dist/services/agent-tools.d.ts +4 -3
- package/dist/services/agent-tools.js +32 -79
- package/dist/services/agent.d.ts +5 -6
- package/dist/services/agent.js +11 -15
- package/dist/services/ai.d.ts +21 -1
- package/dist/services/ai.js +236 -11
- package/dist/services/chatHistoryService.d.ts +95 -2
- package/dist/services/chatHistoryService.js +236 -9
- package/dist/services/contextAgent.js +196 -81
- package/dist/services/investigationComplexity.d.ts +1 -1
- package/dist/services/investigationComplexity.js +1 -1
- package/dist/services/orchestration/investigationAgent.js +101 -84
- package/dist/services/orchestration/investigationCache.d.ts +80 -5
- package/dist/services/orchestration/investigationCache.js +570 -41
- package/dist/services/orchestration/investigationOrchestrator.js +17 -4
- package/dist/services/orchestration/orchestrator.js +6 -3
- package/dist/services/orchestration/scopedTools.js +5 -0
- package/dist/services/orchestration/subAgent.d.ts +31 -1
- package/dist/services/orchestration/subAgent.js +153 -2
- package/dist/utils/analysisRunner.d.ts +29 -0
- package/dist/utils/analysisRunner.js +200 -5
- package/dist/utils/contextPrompts.d.ts +20 -4
- package/dist/utils/contextPrompts.js +158 -23
- package/dist/utils/historyPrompt.d.ts +92 -1
- package/dist/utils/historyPrompt.js +166 -2
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +946 -0
- package/dist/utils/systemPrompts.d.ts +5 -4
- package/dist/utils/systemPrompts.js +46 -14
- package/oclif.manifest.json +1 -1
- 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
|
-
*
|
|
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 ===
|
|
207
|
+
const index = sessions.findIndex((s) => s.id === prunedSession.id);
|
|
47
208
|
if (index >= 0) {
|
|
48
|
-
sessions[index] =
|
|
209
|
+
sessions[index] = prunedSession;
|
|
49
210
|
}
|
|
50
211
|
else {
|
|
51
|
-
sessions.push(
|
|
212
|
+
sessions.push(prunedSession);
|
|
52
213
|
}
|
|
53
|
-
// Keep
|
|
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 {
|