minovative-mind-cli 1.5.1 → 2.0.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 +51 -45
- package/dist/commands/chat.js +7 -2
- package/dist/services/agent/slashCommands.js +156 -30
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +145 -21
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +285 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +167 -35
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +95 -14
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +359 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +214 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +187 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +3 -2
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.js +7 -7
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +106 -5
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- package/package.json +4 -3
|
@@ -1,16 +1,62 @@
|
|
|
1
1
|
import { readCache, writeCache } from '../utils/projectStorage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Service class responsible for tracking, persisting, and reverting file modifications in the workspace.
|
|
4
|
+
* Uses a local project state file (`revert_state.json`) to persist state across CLI invocations.
|
|
5
|
+
*/
|
|
2
6
|
class ChangeLogger {
|
|
7
|
+
/**
|
|
8
|
+
* Ordered history stack of committed changesets, oldest first, newest last.
|
|
9
|
+
*/
|
|
3
10
|
changeStack = [];
|
|
11
|
+
/**
|
|
12
|
+
* The active changeset currently receiving recorded modifications.
|
|
13
|
+
* Null if no change transaction is actively in progress.
|
|
14
|
+
*/
|
|
4
15
|
currentChangeSet = null;
|
|
16
|
+
/**
|
|
17
|
+
* Absolute path to the active workspace root directory.
|
|
18
|
+
*/
|
|
5
19
|
workspaceRoot = '';
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of historical changesets retained in memory and on disk.
|
|
22
|
+
* Older changesets are discarded on save once this limit is exceeded.
|
|
23
|
+
*/
|
|
6
24
|
MAX_HISTORY = 10;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the change logger is currently enabled.
|
|
27
|
+
*/
|
|
28
|
+
isEnabled = true;
|
|
29
|
+
getIsEnabled() {
|
|
30
|
+
return this.isEnabled;
|
|
31
|
+
}
|
|
32
|
+
setIsEnabled(enabled) {
|
|
33
|
+
this.isEnabled = enabled;
|
|
34
|
+
this.saveState();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initializes the ChangeLogger by setting the workspace root and loading existing
|
|
38
|
+
* history from the local `.minovativemind/revert_state.json` cache file.
|
|
39
|
+
*
|
|
40
|
+
* @param workspaceRoot - The absolute path to the project workspace root.
|
|
41
|
+
*/
|
|
7
42
|
init(workspaceRoot) {
|
|
8
43
|
this.workspaceRoot = workspaceRoot;
|
|
9
44
|
const savedState = readCache(workspaceRoot, 'revert_state.json');
|
|
10
|
-
if (savedState
|
|
11
|
-
|
|
45
|
+
if (savedState) {
|
|
46
|
+
if (Array.isArray(savedState)) {
|
|
47
|
+
this.changeStack = savedState;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.changeStack = savedState.history || [];
|
|
51
|
+
this.isEnabled = savedState.isEnabled ?? true;
|
|
52
|
+
}
|
|
12
53
|
}
|
|
13
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Persists the current committed change stack to the local cache directory.
|
|
57
|
+
* Enforces the `MAX_HISTORY` retention policy by slicing out older entries.
|
|
58
|
+
* Does nothing if the workspace root has not yet been initialized.
|
|
59
|
+
*/
|
|
14
60
|
saveState() {
|
|
15
61
|
if (!this.workspaceRoot)
|
|
16
62
|
return;
|
|
@@ -18,11 +64,27 @@ class ChangeLogger {
|
|
|
18
64
|
if (this.changeStack.length > this.MAX_HISTORY) {
|
|
19
65
|
this.changeStack = this.changeStack.slice(-this.MAX_HISTORY);
|
|
20
66
|
}
|
|
21
|
-
writeCache(this.workspaceRoot, 'revert_state.json',
|
|
67
|
+
writeCache(this.workspaceRoot, 'revert_state.json', {
|
|
68
|
+
history: this.changeStack,
|
|
69
|
+
isEnabled: this.isEnabled
|
|
70
|
+
});
|
|
22
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves a copy of the active history stack.
|
|
74
|
+
*
|
|
75
|
+
* @returns A shallow copy of the array containing all tracked changesets.
|
|
76
|
+
*/
|
|
23
77
|
getHistory() {
|
|
24
78
|
return [...this.changeStack];
|
|
25
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Traverses backward through the history stack, popping and collecting changesets
|
|
82
|
+
* until a changeset matching the specified timestamp is encountered (inclusive).
|
|
83
|
+
* Persists the resulting truncated stack after execution.
|
|
84
|
+
*
|
|
85
|
+
* @param timestamp - The unique timestamp identifier of the target changeset to revert back to.
|
|
86
|
+
* @returns An array of popped changesets ordered from newest to oldest.
|
|
87
|
+
*/
|
|
26
88
|
popUntil(timestamp) {
|
|
27
89
|
const changesToRevert = [];
|
|
28
90
|
while (this.changeStack.length > 0) {
|
|
@@ -36,7 +98,15 @@ class ChangeLogger {
|
|
|
36
98
|
this.saveState();
|
|
37
99
|
return changesToRevert;
|
|
38
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Initiates a new transactional changeset. If a previous changeset was in progress and
|
|
103
|
+
* uncommitted, it is automatically finalized and committed to the history stack first.
|
|
104
|
+
*
|
|
105
|
+
* @param description - Descriptive label summarizing the purpose of the new changeset.
|
|
106
|
+
*/
|
|
39
107
|
startChangeSet(description) {
|
|
108
|
+
if (!this.isEnabled)
|
|
109
|
+
return;
|
|
40
110
|
if (this.currentChangeSet) {
|
|
41
111
|
this.commitChangeSet();
|
|
42
112
|
}
|
|
@@ -47,7 +117,23 @@ class ChangeLogger {
|
|
|
47
117
|
status: 'partial',
|
|
48
118
|
};
|
|
49
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Records a file change within the active changeset.
|
|
122
|
+
* If no changeset is active, an anonymous default changeset is automatically initialized.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* Idempotency Guard: If the specified file already has an entry in the current changeset,
|
|
126
|
+
* the original state is preserved. This ensures that multiple successive modifications to
|
|
127
|
+
* the same file within a single transaction map back to the true original state prior to
|
|
128
|
+
* the onset of the transaction.
|
|
129
|
+
*
|
|
130
|
+
* @param filePath - The relative or absolute path of the file being altered.
|
|
131
|
+
* @param originalContent - The text content of the file prior to the change, or null if created.
|
|
132
|
+
* @param action - The classification of the operation ('create', 'modify', or 'delete').
|
|
133
|
+
*/
|
|
50
134
|
logChange(filePath, originalContent, action) {
|
|
135
|
+
if (!this.isEnabled)
|
|
136
|
+
return;
|
|
51
137
|
if (!this.currentChangeSet) {
|
|
52
138
|
// Create a default changeset if none was started explicitly
|
|
53
139
|
this.startChangeSet('Anonymous change');
|
|
@@ -67,11 +153,30 @@ class ChangeLogger {
|
|
|
67
153
|
action,
|
|
68
154
|
});
|
|
69
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Marks the status of the active changeset as 'complete', indicating that all scheduled
|
|
158
|
+
* operations within the scope of this transaction finished without interruption.
|
|
159
|
+
*/
|
|
70
160
|
markComplete() {
|
|
71
161
|
if (this.currentChangeSet) {
|
|
72
162
|
this.currentChangeSet.status = 'complete';
|
|
73
163
|
}
|
|
74
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Retrieves a list of file paths that have been modified in the currently active changeset.
|
|
167
|
+
*
|
|
168
|
+
* @returns An array of string file paths, or an empty array if no changeset is active.
|
|
169
|
+
*/
|
|
170
|
+
getChangedFiles() {
|
|
171
|
+
if (!this.currentChangeSet)
|
|
172
|
+
return [];
|
|
173
|
+
return this.currentChangeSet.changes.map((c) => c.filePath);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Commits the active changeset to the history stack and persists the state to disk,
|
|
177
|
+
* provided that the changeset actually contains one or more recorded file modifications.
|
|
178
|
+
* Resets the active changeset state to null.
|
|
179
|
+
*/
|
|
75
180
|
commitChangeSet() {
|
|
76
181
|
if (this.currentChangeSet && this.currentChangeSet.changes.length > 0) {
|
|
77
182
|
this.changeStack.push(this.currentChangeSet);
|
|
@@ -79,14 +184,30 @@ class ChangeLogger {
|
|
|
79
184
|
}
|
|
80
185
|
this.currentChangeSet = null;
|
|
81
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Retrieves the most recently committed changeset from the history stack without removing it.
|
|
189
|
+
*
|
|
190
|
+
* @returns The latest committed ChangeSet, or null if the stack is currently empty.
|
|
191
|
+
*/
|
|
82
192
|
getLastChangeSet() {
|
|
83
193
|
if (this.changeStack.length === 0)
|
|
84
194
|
return null;
|
|
85
195
|
return this.changeStack[this.changeStack.length - 1];
|
|
86
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Returns the active, uncommitted changeset currently receiving modifications.
|
|
199
|
+
*
|
|
200
|
+
* @returns The in-progress ChangeSet, or null if no changeset is active.
|
|
201
|
+
*/
|
|
87
202
|
getCurrentChangeSet() {
|
|
88
203
|
return this.currentChangeSet;
|
|
89
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Removes and returns the most recently committed changeset from the history stack.
|
|
207
|
+
* Persists the newly truncated stack state back to the cache.
|
|
208
|
+
*
|
|
209
|
+
* @returns The removed ChangeSet, or null if no historical changes exist.
|
|
210
|
+
*/
|
|
90
211
|
popLastChangeSet() {
|
|
91
212
|
if (this.changeStack.length === 0)
|
|
92
213
|
return null;
|
|
@@ -96,8 +217,16 @@ class ChangeLogger {
|
|
|
96
217
|
}
|
|
97
218
|
return popped;
|
|
98
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Checks whether the history stack contains any tracked changesets.
|
|
222
|
+
*
|
|
223
|
+
* @returns True if at least one changeset is recorded; false otherwise.
|
|
224
|
+
*/
|
|
99
225
|
hasChanges() {
|
|
100
226
|
return this.changeStack.length > 0;
|
|
101
227
|
}
|
|
102
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Singleton instance of the ChangeLogger service exported for application-wide tracking.
|
|
231
|
+
*/
|
|
103
232
|
export const changeLogger = new ChangeLogger();
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
export interface ContextAgentResult {
|
|
2
2
|
projectTree: string;
|
|
3
3
|
projectType: string;
|
|
4
|
-
relevantFiles: Map<string,
|
|
4
|
+
relevantFiles: Map<string, {
|
|
5
|
+
text: string;
|
|
6
|
+
inlineData?: any;
|
|
7
|
+
}>;
|
|
5
8
|
summary: string;
|
|
6
9
|
webSearchSummary?: string;
|
|
10
|
+
isParallel?: boolean;
|
|
7
11
|
}
|
|
8
12
|
export interface IntentRoute {
|
|
9
13
|
needsContext: boolean;
|
|
10
14
|
targetAgent: 'CHAT' | 'EXECUTE';
|
|
11
15
|
}
|
|
12
16
|
export declare function routeIntent(userRequest: string, chatHistory?: string): Promise<IntentRoute>;
|
|
17
|
+
export declare function evaluateExecutionComplexity(userRequest: string, investigationSummary: string | undefined, numRelevantFiles: number, chatHistory?: string): Promise<'EASY' | 'HARD'>;
|
|
13
18
|
export declare function gatherContext(workspaceRoot: string, userRequest: string, chatHistory: string | undefined, inputHandler: {
|
|
14
19
|
getAndClear: () => string;
|
|
15
20
|
waitForPrompt: () => Promise<void>;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession } from './ai.js';
|
|
5
|
-
import {
|
|
4
|
+
import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession, createExecutionComplexitySession, } from './ai.js';
|
|
5
|
+
import { evaluateInvestigationComplexity } from './investigationComplexity.js';
|
|
6
|
+
import { InvestigationOrchestrator } from './orchestration/investigationOrchestrator.js';
|
|
7
|
+
import { isSubAgentsEnabled, listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from './agent-tools.js';
|
|
6
8
|
import { debugLog } from '../utils/logger.js';
|
|
7
9
|
import { buildDependencyGraph } from '../utils/dependencyTracer.js';
|
|
8
10
|
import { runEphemeralScript } from '../utils/analysisRunner.js';
|
|
11
|
+
import { getMetricCollector } from './metrics.js';
|
|
12
|
+
const metrics = getMetricCollector();
|
|
9
13
|
async function detectProjectType(workspaceRoot) {
|
|
10
14
|
const types = [];
|
|
11
15
|
const fileExists = async (fileName) => {
|
|
@@ -157,6 +161,26 @@ export async function routeIntent(userRequest, chatHistory = '') {
|
|
|
157
161
|
return { needsContext: true, targetAgent: 'EXECUTE' };
|
|
158
162
|
}
|
|
159
163
|
}
|
|
164
|
+
export async function evaluateExecutionComplexity(userRequest, investigationSummary, numRelevantFiles, chatHistory = '') {
|
|
165
|
+
try {
|
|
166
|
+
const session = createExecutionComplexitySession();
|
|
167
|
+
let prompt = `User Request: "${userRequest}"
|
|
168
|
+
Investigation Summary: ${investigationSummary || 'None (0 files needed)'}
|
|
169
|
+
Number of Relevant Files: ${numRelevantFiles}`;
|
|
170
|
+
if (chatHistory) {
|
|
171
|
+
prompt = `Previous Conversation Context:\n${chatHistory}\n\n${prompt}`;
|
|
172
|
+
}
|
|
173
|
+
const result = await session.sendMessage(prompt);
|
|
174
|
+
const text = result.response.text()?.trim() || '{}';
|
|
175
|
+
const parsed = JSON.parse(text);
|
|
176
|
+
debugLog(`Execution Complexity Parsed: ${JSON.stringify(parsed)}`);
|
|
177
|
+
return parsed.complexity === 'EASY' ? 'EASY' : 'HARD';
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
debugLog(`Execution Complexity Router failed, falling back to HARD. Error: ${String(e)}`);
|
|
181
|
+
return 'HARD';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
160
184
|
export async function gatherContext(workspaceRoot, userRequest, chatHistory = '', inputHandler, abortSignal, onProgress) {
|
|
161
185
|
// Always skip slash commands for zero latency
|
|
162
186
|
if (userRequest.startsWith('/')) {
|
|
@@ -174,6 +198,23 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
174
198
|
projectTree = projectTree.substring(0, 30000) + '\n... (Project tree truncated due to size)';
|
|
175
199
|
}
|
|
176
200
|
const projectType = await detectProjectType(workspaceRoot);
|
|
201
|
+
// ─── Parallel Investigation Gate ─────────────────────────────
|
|
202
|
+
if (isSubAgentsEnabled()) {
|
|
203
|
+
// Determine complexity and domain breakdown
|
|
204
|
+
const approxFiles = projectTree.split('\n').length;
|
|
205
|
+
const complexity = await evaluateInvestigationComplexity(userRequest, projectType, approxFiles, chatHistory);
|
|
206
|
+
if (complexity.strategy === 'PARALLEL' && complexity.agentAssignments.length > 0) {
|
|
207
|
+
const orchestrator = new InvestigationOrchestrator();
|
|
208
|
+
const parallelResult = await orchestrator.runParallelInvestigation(userRequest, complexity.agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress);
|
|
209
|
+
if (parallelResult !== null) {
|
|
210
|
+
// If parallel investigation succeeded, we are done. Return immediately.
|
|
211
|
+
parallelResult.isParallel = true;
|
|
212
|
+
return { contextResult: parallelResult, targetAgent, chainedMessages: [] };
|
|
213
|
+
}
|
|
214
|
+
// If it returned null, all agents crashed. Fall through to single agent fallback.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ─── Single Agent Path (Fallback/Default) ────────────────────
|
|
177
218
|
const session = createContextAgentSession();
|
|
178
219
|
const relevantFiles = new Map();
|
|
179
220
|
let summary = 'No relevant context found.';
|
|
@@ -185,7 +226,8 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
185
226
|
currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
|
|
186
227
|
}
|
|
187
228
|
currentMessage += `\n\nStart investigating to find relevant files.`;
|
|
188
|
-
const MAX_TURNS =
|
|
229
|
+
const MAX_TURNS = Infinity;
|
|
230
|
+
let isInvestigationFinished = false;
|
|
189
231
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
190
232
|
await inputHandler.waitForPrompt();
|
|
191
233
|
const queuedMsg = inputHandler.getAndClear();
|
|
@@ -276,11 +318,12 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
276
318
|
if (!relevantFiles.has(filePath)) {
|
|
277
319
|
const readResult = await readFile(workspaceRoot, filePath);
|
|
278
320
|
if (!readResult.error) {
|
|
279
|
-
relevantFiles.set(filePath, readResult.output);
|
|
321
|
+
relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
280
322
|
}
|
|
281
323
|
}
|
|
282
324
|
}
|
|
283
325
|
isFinished = true;
|
|
326
|
+
isInvestigationFinished = true;
|
|
284
327
|
// ── Auto-trace reverse dependencies ──
|
|
285
328
|
// When the Context Agent finalizes its investigation, we automatically
|
|
286
329
|
// discover files that DEPEND ON the selected files. This ensures the
|
|
@@ -306,7 +349,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
306
349
|
if (!relevantFiles.has(dep)) {
|
|
307
350
|
const readResult = await readFile(workspaceRoot, dep);
|
|
308
351
|
if (!readResult.error) {
|
|
309
|
-
relevantFiles.set(dep, readResult.output);
|
|
352
|
+
relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
|
|
310
353
|
added++;
|
|
311
354
|
}
|
|
312
355
|
}
|
|
@@ -336,7 +379,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
336
379
|
for (const filePath of filesToRead) {
|
|
337
380
|
const readResult = await readFile(workspaceRoot, filePath);
|
|
338
381
|
if (!readResult.error) {
|
|
339
|
-
relevantFiles.set(filePath, readResult.output);
|
|
382
|
+
relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
340
383
|
output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
|
|
341
384
|
}
|
|
342
385
|
else {
|
|
@@ -371,10 +414,47 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
371
414
|
},
|
|
372
415
|
});
|
|
373
416
|
}
|
|
417
|
+
else if (call.name === 'semantic_search') {
|
|
418
|
+
const query = args.query;
|
|
419
|
+
const topK = args.topK || 5;
|
|
420
|
+
let output = '';
|
|
421
|
+
try {
|
|
422
|
+
const { getEmbeddingIndex } = await import('./embeddingIndex.js');
|
|
423
|
+
const index = getEmbeddingIndex();
|
|
424
|
+
if (!index.isReady()) {
|
|
425
|
+
// Lazy load from disk or build if missing
|
|
426
|
+
const loaded = await index.load(workspaceRoot);
|
|
427
|
+
if (!loaded) {
|
|
428
|
+
if (onProgress)
|
|
429
|
+
onProgress('Building semantic search index (first run)...');
|
|
430
|
+
await index.buildIndex(workspaceRoot, onProgress);
|
|
431
|
+
await index.save(workspaceRoot);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const results = await index.search(query, topK);
|
|
435
|
+
if (results.length === 0) {
|
|
436
|
+
output = 'No semantically similar code found. (Index might be empty or embedding failed)';
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
output = results
|
|
440
|
+
.map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
|
|
441
|
+
.join('\n---\n');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
output = `Semantic search failed: ${e.message}`;
|
|
446
|
+
}
|
|
447
|
+
functionResponses.push({
|
|
448
|
+
functionResponse: {
|
|
449
|
+
name: call.name,
|
|
450
|
+
response: { output },
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
374
454
|
else if (call.name === 'read_file') {
|
|
375
455
|
const readRes = await readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
|
|
376
456
|
if (!readRes.error) {
|
|
377
|
-
relevantFiles.set(args.filePath, readRes.output);
|
|
457
|
+
relevantFiles.set(args.filePath, { text: readRes.output, inlineData: readRes.inlineData });
|
|
378
458
|
}
|
|
379
459
|
functionResponses.push({
|
|
380
460
|
functionResponse: {
|
|
@@ -452,14 +532,15 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
452
532
|
// Prepare next turn
|
|
453
533
|
currentMessage = functionResponses;
|
|
454
534
|
}
|
|
535
|
+
const collector = getMetricCollector();
|
|
536
|
+
if (collector) {
|
|
537
|
+
collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
|
|
538
|
+
if (!isInvestigationFinished || relevantFiles.size === 0) {
|
|
539
|
+
collector.recordInvestigationFailure();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
455
542
|
return {
|
|
456
|
-
contextResult: {
|
|
457
|
-
projectTree,
|
|
458
|
-
projectType,
|
|
459
|
-
relevantFiles,
|
|
460
|
-
summary,
|
|
461
|
-
webSearchSummary,
|
|
462
|
-
},
|
|
543
|
+
contextResult: { projectTree, projectType, relevantFiles, summary, webSearchSummary, isParallel: false },
|
|
463
544
|
targetAgent,
|
|
464
545
|
chainedMessages,
|
|
465
546
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single result from a semantic search query.
|
|
3
|
+
*/
|
|
4
|
+
export interface SearchResult {
|
|
5
|
+
filePath: string;
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
label: string;
|
|
9
|
+
preview: string;
|
|
10
|
+
/** Cosine similarity score between 0.0 and 1.0 */
|
|
11
|
+
score: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Manages a persistent local vector index for semantic code search.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle:
|
|
17
|
+
* 1. On first `gatherContext` call, if no index exists on disk, `buildIndex()` is called.
|
|
18
|
+
* 2. After the Execution Agent modifies files, `updateIndex()` delta-reindexes only changed files.
|
|
19
|
+
* 3. `search()` embeds the query (1 API call) and scans the local index via cosine similarity.
|
|
20
|
+
* 4. Index is persisted to `.minovativemind/embeddings/index.json` via atomic writes.
|
|
21
|
+
*/
|
|
22
|
+
export declare class EmbeddingIndex {
|
|
23
|
+
private chunks;
|
|
24
|
+
private modelVersion;
|
|
25
|
+
/**
|
|
26
|
+
* Builds the full index for a workspace by scanning all eligible source files,
|
|
27
|
+
* chunking them at function/class boundaries, and batch-embedding them.
|
|
28
|
+
*
|
|
29
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
30
|
+
* @param onProgress - Optional callback for user-facing progress messages.
|
|
31
|
+
*/
|
|
32
|
+
buildIndex(workspaceRoot: string, onProgress?: (msg: string) => void): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Delta-updates the index for files that have changed.
|
|
35
|
+
* Removes stale chunks for modified/deleted files, then re-chunks and
|
|
36
|
+
* re-embeds only the affected files.
|
|
37
|
+
*
|
|
38
|
+
* @param workspaceRoot - Absolute path to the workspace root.
|
|
39
|
+
* @param changedFiles - Array of relative file paths that were modified.
|
|
40
|
+
*/
|
|
41
|
+
updateIndex(workspaceRoot: string, changedFiles: string[]): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Executes a semantic search against the local index.
|
|
44
|
+
*
|
|
45
|
+
* 1. Embeds the query string (single API call).
|
|
46
|
+
* 2. Computes cosine similarity against all indexed chunks.
|
|
47
|
+
* 3. Returns the top-K results sorted by descending similarity score.
|
|
48
|
+
*
|
|
49
|
+
* @param query - Natural language description of what to search for.
|
|
50
|
+
* @param topK - Number of results to return (default 5, max 15).
|
|
51
|
+
* @returns Array of search results with file paths, line ranges, and scores.
|
|
52
|
+
*/
|
|
53
|
+
search(query: string, topK?: number): Promise<SearchResult[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Persists the index to disk using the atomic write pattern from projectStorage.
|
|
56
|
+
*/
|
|
57
|
+
save(workspaceRoot: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Loads the index from disk. Returns false if no index exists or the model
|
|
60
|
+
* version has changed (requiring a full rebuild).
|
|
61
|
+
*/
|
|
62
|
+
load(workspaceRoot: string): Promise<boolean>;
|
|
63
|
+
/** Check if the index is loaded and contains chunks */
|
|
64
|
+
isReady(): boolean;
|
|
65
|
+
/** Get the number of chunks in the index */
|
|
66
|
+
get size(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Recursively walks the workspace, reading and chunking eligible files.
|
|
69
|
+
* Respects .gitignore patterns and EXCLUDED_EXTENSIONS.
|
|
70
|
+
*/
|
|
71
|
+
private walkAndChunk;
|
|
72
|
+
/**
|
|
73
|
+
* Generates a content hash for delta-detection.
|
|
74
|
+
* Uses SHA-256 of (filePath + content) to detect changes.
|
|
75
|
+
*/
|
|
76
|
+
private hashChunk;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns the global singleton EmbeddingIndex instance.
|
|
80
|
+
* Lazily created on first access.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getEmbeddingIndex(): EmbeddingIndex;
|