minovative-mind-cli 1.5.1 → 2.1.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 +59 -45
- package/dist/commands/chat.js +10 -2
- package/dist/services/agent/slashCommands.js +369 -42
- 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 +272 -34
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +288 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +182 -36
- 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 +112 -19
- 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 +362 -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 +217 -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 +190 -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/services/workspaceRegistry.d.ts +137 -0
- package/dist/services/workspaceRegistry.js +270 -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 +10 -3
- 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/pathSecurity.d.ts +31 -0
- package/dist/utils/pathSecurity.js +48 -0
- 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 +111 -6
- 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('/')) {
|
|
@@ -168,12 +192,41 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
168
192
|
if (!needsContext) {
|
|
169
193
|
return { contextResult: null, targetAgent, chainedMessages: [] };
|
|
170
194
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
const { workspaceRegistry } = await import('./workspaceRegistry.js');
|
|
196
|
+
const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
|
|
197
|
+
let projectTree = '';
|
|
198
|
+
let primaryProjectType = 'Unknown';
|
|
199
|
+
for (const { alias, root } of allRoots) {
|
|
200
|
+
const label = alias ? `@${alias} (${root})` : `Primary Workspace (${root})`;
|
|
201
|
+
const treeResult = await listDirectory(root, '.', 10);
|
|
202
|
+
let tree = treeResult.output;
|
|
203
|
+
if (tree.length > 30000) {
|
|
204
|
+
tree = tree.substring(0, 30000) + '\\n... (Project tree truncated due to size)';
|
|
205
|
+
}
|
|
206
|
+
const type = await detectProjectType(root);
|
|
207
|
+
if (!alias) {
|
|
208
|
+
primaryProjectType = type;
|
|
209
|
+
}
|
|
210
|
+
projectTree += `=== ${label} ===\\nProject Type: ${type}\\n${tree}\\n\\n`;
|
|
175
211
|
}
|
|
176
|
-
const projectType =
|
|
212
|
+
const projectType = primaryProjectType;
|
|
213
|
+
// ─── Parallel Investigation Gate ─────────────────────────────
|
|
214
|
+
if (isSubAgentsEnabled()) {
|
|
215
|
+
// Determine complexity and domain breakdown
|
|
216
|
+
const approxFiles = projectTree.split('\n').length;
|
|
217
|
+
const complexity = await evaluateInvestigationComplexity(userRequest, projectType, approxFiles, chatHistory);
|
|
218
|
+
if (complexity.strategy === 'PARALLEL' && complexity.agentAssignments.length > 0) {
|
|
219
|
+
const orchestrator = new InvestigationOrchestrator();
|
|
220
|
+
const parallelResult = await orchestrator.runParallelInvestigation(userRequest, complexity.agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress);
|
|
221
|
+
if (parallelResult !== null) {
|
|
222
|
+
// If parallel investigation succeeded, we are done. Return immediately.
|
|
223
|
+
parallelResult.isParallel = true;
|
|
224
|
+
return { contextResult: parallelResult, targetAgent, chainedMessages: [] };
|
|
225
|
+
}
|
|
226
|
+
// If it returned null, all agents crashed. Fall through to single agent fallback.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ─── Single Agent Path (Fallback/Default) ────────────────────
|
|
177
230
|
const session = createContextAgentSession();
|
|
178
231
|
const relevantFiles = new Map();
|
|
179
232
|
let summary = 'No relevant context found.';
|
|
@@ -185,7 +238,8 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
185
238
|
currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
|
|
186
239
|
}
|
|
187
240
|
currentMessage += `\n\nStart investigating to find relevant files.`;
|
|
188
|
-
const MAX_TURNS =
|
|
241
|
+
const MAX_TURNS = Infinity;
|
|
242
|
+
let isInvestigationFinished = false;
|
|
189
243
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
190
244
|
await inputHandler.waitForPrompt();
|
|
191
245
|
const queuedMsg = inputHandler.getAndClear();
|
|
@@ -276,11 +330,12 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
276
330
|
if (!relevantFiles.has(filePath)) {
|
|
277
331
|
const readResult = await readFile(workspaceRoot, filePath);
|
|
278
332
|
if (!readResult.error) {
|
|
279
|
-
relevantFiles.set(filePath, readResult.output);
|
|
333
|
+
relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
280
334
|
}
|
|
281
335
|
}
|
|
282
336
|
}
|
|
283
337
|
isFinished = true;
|
|
338
|
+
isInvestigationFinished = true;
|
|
284
339
|
// ── Auto-trace reverse dependencies ──
|
|
285
340
|
// When the Context Agent finalizes its investigation, we automatically
|
|
286
341
|
// discover files that DEPEND ON the selected files. This ensures the
|
|
@@ -306,7 +361,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
306
361
|
if (!relevantFiles.has(dep)) {
|
|
307
362
|
const readResult = await readFile(workspaceRoot, dep);
|
|
308
363
|
if (!readResult.error) {
|
|
309
|
-
relevantFiles.set(dep, readResult.output);
|
|
364
|
+
relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
|
|
310
365
|
added++;
|
|
311
366
|
}
|
|
312
367
|
}
|
|
@@ -336,7 +391,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
336
391
|
for (const filePath of filesToRead) {
|
|
337
392
|
const readResult = await readFile(workspaceRoot, filePath);
|
|
338
393
|
if (!readResult.error) {
|
|
339
|
-
relevantFiles.set(filePath, readResult.output);
|
|
394
|
+
relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
340
395
|
output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
|
|
341
396
|
}
|
|
342
397
|
else {
|
|
@@ -371,10 +426,47 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
371
426
|
},
|
|
372
427
|
});
|
|
373
428
|
}
|
|
429
|
+
else if (call.name === 'semantic_search') {
|
|
430
|
+
const query = args.query;
|
|
431
|
+
const topK = args.topK || 5;
|
|
432
|
+
let output = '';
|
|
433
|
+
try {
|
|
434
|
+
const { getEmbeddingIndex } = await import('./embeddingIndex.js');
|
|
435
|
+
const index = getEmbeddingIndex();
|
|
436
|
+
if (!index.isReady()) {
|
|
437
|
+
// Lazy load from disk or build if missing
|
|
438
|
+
const loaded = await index.load(workspaceRoot);
|
|
439
|
+
if (!loaded) {
|
|
440
|
+
if (onProgress)
|
|
441
|
+
onProgress('Building semantic search index (first run)...');
|
|
442
|
+
await index.buildIndex(workspaceRoot, onProgress);
|
|
443
|
+
await index.save(workspaceRoot);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const results = await index.search(query, topK);
|
|
447
|
+
if (results.length === 0) {
|
|
448
|
+
output = 'No semantically similar code found. (Index might be empty or embedding failed)';
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
output = results
|
|
452
|
+
.map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
|
|
453
|
+
.join('\n---\n');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
output = `Semantic search failed: ${e.message}`;
|
|
458
|
+
}
|
|
459
|
+
functionResponses.push({
|
|
460
|
+
functionResponse: {
|
|
461
|
+
name: call.name,
|
|
462
|
+
response: { output },
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
374
466
|
else if (call.name === 'read_file') {
|
|
375
467
|
const readRes = await readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
|
|
376
468
|
if (!readRes.error) {
|
|
377
|
-
relevantFiles.set(args.filePath, readRes.output);
|
|
469
|
+
relevantFiles.set(args.filePath, { text: readRes.output, inlineData: readRes.inlineData });
|
|
378
470
|
}
|
|
379
471
|
functionResponses.push({
|
|
380
472
|
functionResponse: {
|
|
@@ -452,14 +544,15 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
452
544
|
// Prepare next turn
|
|
453
545
|
currentMessage = functionResponses;
|
|
454
546
|
}
|
|
547
|
+
const collector = getMetricCollector();
|
|
548
|
+
if (collector) {
|
|
549
|
+
collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
|
|
550
|
+
if (!isInvestigationFinished || relevantFiles.size === 0) {
|
|
551
|
+
collector.recordInvestigationFailure();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
455
554
|
return {
|
|
456
|
-
contextResult: {
|
|
457
|
-
projectTree,
|
|
458
|
-
projectType,
|
|
459
|
-
relevantFiles,
|
|
460
|
-
summary,
|
|
461
|
-
webSearchSummary,
|
|
462
|
-
},
|
|
555
|
+
contextResult: { projectTree, projectType, relevantFiles, summary, webSearchSummary, isParallel: false },
|
|
463
556
|
targetAgent,
|
|
464
557
|
chainedMessages,
|
|
465
558
|
};
|
|
@@ -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;
|