minovative-mind-cli 2.5.2 → 2.6.1
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 +29 -26
- package/dist/commands/chat.js +2 -2
- package/dist/services/agent/inputHandler.d.ts +9 -0
- package/dist/services/agent/inputHandler.js +34 -0
- package/dist/services/agent/slashCommands.js +105 -29
- package/dist/services/agent/syntaxAgent.d.ts +40 -0
- package/dist/services/agent/syntaxAgent.js +237 -23
- package/dist/services/agent/toolLoop.js +10 -1
- package/dist/services/agent/types.d.ts +1 -0
- package/dist/services/agent-tools.d.ts +156 -1
- package/dist/services/agent-tools.js +259 -67
- package/dist/services/agent.d.ts +74 -0
- package/dist/services/agent.js +193 -31
- package/dist/services/ai.d.ts +5 -0
- package/dist/services/ai.js +80 -87
- package/dist/services/chatHistoryService.d.ts +11 -0
- package/dist/services/chatHistoryService.js +20 -1
- package/dist/services/contextAgent.d.ts +1 -1
- package/dist/services/contextAgent.js +9 -29
- package/dist/services/orchestration/investigationAgent.d.ts +2 -1
- package/dist/services/orchestration/investigationAgent.js +7 -2
- package/dist/services/orchestration/investigationOrchestrator.d.ts +1 -1
- package/dist/services/orchestration/investigationOrchestrator.js +13 -2
- package/dist/services/orchestration/orchestrator.js +21 -4
- package/dist/services/orchestration/subAgent.d.ts +2 -1
- package/dist/services/orchestration/subAgent.js +12 -6
- package/dist/utils/analysisRunner.d.ts +27 -4
- package/dist/utils/analysisRunner.js +100 -20
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +2 -0
- package/dist/utils/fuzzyMatch.d.ts +32 -0
- package/dist/utils/fuzzyMatch.js +215 -27
- package/dist/utils/localSyntaxValidator.d.ts +2 -2
- package/dist/utils/localSyntaxValidator.js +280 -81
- package/dist/utils/performanceAuditor.d.ts +2 -7
- package/dist/utils/performanceAuditor.js +541 -89
- package/dist/utils/projectStorage.js +9 -0
- package/dist/utils/systemPrompts.d.ts +3 -2
- package/dist/utils/systemPrompts.js +29 -5
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file agent-tools.ts
|
|
3
|
+
* @description Implements tool declarations, approval state management, ignore rule parser,
|
|
4
|
+
* atomic file operations, search/grep engines, dependency tracer, scratchpad script runner,
|
|
5
|
+
* and central tool dispatcher used by the Minovative Mind AI agents.
|
|
6
|
+
*/
|
|
1
7
|
import { exec } from 'node:child_process';
|
|
2
8
|
import { promises as fs } from 'node:fs';
|
|
3
9
|
import os from 'node:os';
|
|
@@ -9,7 +15,7 @@ import { workspaceRegistry } from './workspaceRegistry.js';
|
|
|
9
15
|
import { changeLogger } from './changeLogger.js';
|
|
10
16
|
import { findBestMatch, applyMatch } from '../utils/fuzzyMatch.js';
|
|
11
17
|
import { localValidate } from '../utils/localSyntaxValidator.js';
|
|
12
|
-
import { validateAndFixSyntax } from './agent/syntaxAgent.js';
|
|
18
|
+
import { validateAndFixSyntax, aiFuzzyMatch } from './agent/syntaxAgent.js';
|
|
13
19
|
import { sanitizeForCDATA } from '../utils/contextPrompts.js';
|
|
14
20
|
import { findDependencies, formatDependencyResult } from '../utils/dependencyTracer.js';
|
|
15
21
|
import { atomicWriteFile } from '../utils/atomicWrite.js';
|
|
@@ -20,6 +26,12 @@ import { getCurrentAgentId } from '../utils/asyncContext.js';
|
|
|
20
26
|
import ignore from 'ignore';
|
|
21
27
|
const execAsync = promisify(exec);
|
|
22
28
|
// ─── Tool Declarations for Gemini Function Calling ───────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Returns the list of available function declarations for Gemini function calling.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Configuration options such as `isExecutionAgent`.
|
|
33
|
+
* @returns An array of Google Generative AI `FunctionDeclaration` objects.
|
|
34
|
+
*/
|
|
23
35
|
export function getToolDeclarations(options) {
|
|
24
36
|
if (options?.isExecutionAgent) {
|
|
25
37
|
return toolDeclarations;
|
|
@@ -31,6 +43,17 @@ export function getToolDeclarations(options) {
|
|
|
31
43
|
* every tool the agent can invoke. Passed to the model at init.
|
|
32
44
|
*/
|
|
33
45
|
export const toolDeclarations = [
|
|
46
|
+
{
|
|
47
|
+
name: 'perform_web_search',
|
|
48
|
+
description: 'Search the web for documentation, solutions, or real-time information.',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: SchemaType.OBJECT,
|
|
51
|
+
properties: {
|
|
52
|
+
query: { type: SchemaType.STRING, description: 'The search query to look up on the web' },
|
|
53
|
+
},
|
|
54
|
+
required: ['query'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
34
57
|
{
|
|
35
58
|
name: 'read_file',
|
|
36
59
|
description: 'Read the contents of a file at the given path relative to the workspace root. Supports text files and native parsing of .pdf files (including math and diagrams). Use startLine and endLine to read specific chunks of massive files to avoid context limits. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
|
|
@@ -110,7 +133,7 @@ export const toolDeclarations = [
|
|
|
110
133
|
},
|
|
111
134
|
{
|
|
112
135
|
name: 'modify_file',
|
|
113
|
-
description: 'Perform one or multiple targeted search-and-replace edits in a single file. CRITICAL REQUIREMENT: You MUST
|
|
136
|
+
description: 'Perform one or multiple targeted search-and-replace edits in a single file. CRITICAL REQUIREMENT: You MUST have the exact current file content before using this tool. If the file is NOT already in your context, you MUST use read_file or grep_search to fetch it. Do NOT guess or hallucinate the searchContent without reading the exact lines first, or the edit will fail. The search strings must match the current file exactly (including whitespace). This is preferred over write_file for editing existing files. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
|
|
114
137
|
parameters: {
|
|
115
138
|
type: SchemaType.OBJECT,
|
|
116
139
|
properties: {
|
|
@@ -174,7 +197,7 @@ export const toolDeclarations = [
|
|
|
174
197
|
},
|
|
175
198
|
{
|
|
176
199
|
name: 'grep_search',
|
|
177
|
-
description: 'Search for a text pattern across files in the workspace. Returns matching file paths with line numbers and content snippets. Uses Extended Regular Expressions (grep -E). IMPORTANT: grep searches line-by-line. Do NOT search for long lists of Tailwind classes or multi-line strings, as they will fail if line-wrapped. Search for short, unique substrings. Use the workspace parameter to search external workspaces.',
|
|
200
|
+
description: 'Search for a text pattern across files in the workspace. Returns matching file paths with line numbers and content snippets. Uses Extended Regular Expressions (grep -E) by default, or fixed strings (grep -F) if fixedStrings is true. IMPORTANT: grep searches line-by-line. Do NOT search for long lists of Tailwind classes or multi-line strings, as they will fail if line-wrapped. Search for short, unique substrings. Use the workspace parameter to search external workspaces.',
|
|
178
201
|
parameters: {
|
|
179
202
|
type: SchemaType.OBJECT,
|
|
180
203
|
properties: {
|
|
@@ -186,6 +209,14 @@ export const toolDeclarations = [
|
|
|
186
209
|
type: SchemaType.STRING,
|
|
187
210
|
description: 'Optional glob to restrict file types, e.g. "*.ts" or "*.py". Defaults to all files.',
|
|
188
211
|
},
|
|
212
|
+
fixedStrings: {
|
|
213
|
+
type: SchemaType.BOOLEAN,
|
|
214
|
+
description: 'Optional. If true, treats the pattern as a fixed string instead of a regular expression (grep -F). Useful for searching code containing regex special characters.',
|
|
215
|
+
},
|
|
216
|
+
dirPath: {
|
|
217
|
+
type: SchemaType.STRING,
|
|
218
|
+
description: 'Optional. The directory path to restrict the search to, relative to the workspace root. Defaults to the workspace root.',
|
|
219
|
+
},
|
|
189
220
|
workspace: {
|
|
190
221
|
type: SchemaType.STRING,
|
|
191
222
|
description: 'Optional. Workspace alias to search in (e.g., "backend"), or "all" to search all registered workspaces. Defaults to the primary workspace.',
|
|
@@ -297,16 +328,36 @@ export const toolDeclarations = [
|
|
|
297
328
|
},
|
|
298
329
|
];
|
|
299
330
|
let currentApprovalMode = 'ask';
|
|
331
|
+
/**
|
|
332
|
+
* Retrieves the current command execution approval mode.
|
|
333
|
+
*
|
|
334
|
+
* @returns The active {@link ApprovalMode}.
|
|
335
|
+
*/
|
|
300
336
|
export function getApprovalMode() {
|
|
301
337
|
return currentApprovalMode;
|
|
302
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Sets the command execution approval mode.
|
|
341
|
+
*
|
|
342
|
+
* @param mode - The {@link ApprovalMode} to set.
|
|
343
|
+
*/
|
|
303
344
|
export function setApprovalMode(mode) {
|
|
304
345
|
currentApprovalMode = mode;
|
|
305
346
|
}
|
|
306
347
|
let subAgentsEnabled = true;
|
|
348
|
+
/**
|
|
349
|
+
* Checks whether sub-agents are currently enabled.
|
|
350
|
+
*
|
|
351
|
+
* @returns `true` if sub-agents are enabled, `false` otherwise.
|
|
352
|
+
*/
|
|
307
353
|
export function isSubAgentsEnabled() {
|
|
308
354
|
return subAgentsEnabled;
|
|
309
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Enables or disables sub-agents.
|
|
358
|
+
*
|
|
359
|
+
* @param enabled - Boolean indicating whether sub-agents should be enabled.
|
|
360
|
+
*/
|
|
310
361
|
export function setSubAgentsEnabled(enabled) {
|
|
311
362
|
subAgentsEnabled = enabled;
|
|
312
363
|
}
|
|
@@ -347,6 +398,9 @@ const DEFAULT_IGNORED_FILES = new Set([
|
|
|
347
398
|
]);
|
|
348
399
|
/**
|
|
349
400
|
* Parses .gitignore and .minovativemindignore to supplement the default ignore lists.
|
|
401
|
+
*
|
|
402
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
403
|
+
* @returns An object containing `ignoredDirs` (Set), `ignoredFiles` (Set), and `ig` (ignore instance).
|
|
350
404
|
*/
|
|
351
405
|
async function getIgnoredPaths(workspaceRoot) {
|
|
352
406
|
const ignoredDirs = new Set(DEFAULT_IGNORED_DIRS);
|
|
@@ -378,6 +432,18 @@ async function getIgnoredPaths(workspaceRoot) {
|
|
|
378
432
|
return { ignoredDirs, ignoredFiles, ig };
|
|
379
433
|
}
|
|
380
434
|
// ─── Tool Implementations ────────────────────────────────────────────
|
|
435
|
+
/**
|
|
436
|
+
* Reads the contents of a file at the given relative or absolute path, supporting text files,
|
|
437
|
+
* native PDF parsing, SQLite database schema dumping, Jupyter Notebook (.ipynb) cell extraction,
|
|
438
|
+
* and CSV-to-Markdown table formatting.
|
|
439
|
+
*
|
|
440
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
441
|
+
* @param filePath - Relative path to the target file (or `@alias/` prefixed path).
|
|
442
|
+
* @param startLine - Optional 1-indexed starting line number.
|
|
443
|
+
* @param endLine - Optional 1-indexed ending line number.
|
|
444
|
+
* @param targetElements - Optional array of specific symbol or function names to extract.
|
|
445
|
+
* @returns A promise resolving to a {@link ToolResult} containing the file content or error details.
|
|
446
|
+
*/
|
|
381
447
|
export async function readFile(workspaceRoot, filePath, startLine, endLine, targetElements) {
|
|
382
448
|
try {
|
|
383
449
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
@@ -573,7 +639,7 @@ export async function readFile(workspaceRoot, filePath, startLine, endLine, targ
|
|
|
573
639
|
}
|
|
574
640
|
const attrLines = startLine || endLine ? ` lines="${startLine || 1}-${endLine || 'end'}"` : '';
|
|
575
641
|
const attrTargets = targetElements && targetElements.length > 0 ? ` elements="${targetElements.join(',')}"` : '';
|
|
576
|
-
const wrappedContent = `<workspace_file path="${filePath}"${attrLines}${attrTargets}>\n<content_data><![CDATA[\n${sanitizeForCDATA(content)}\n]]></content_data>\n</workspace_file>`;
|
|
642
|
+
const wrappedContent = `<workspace_file path="${filePath}"${attrLines}${attrTargets}>\n<content_data><![CDATA[\n${sanitizeForCDATA(content)}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
577
643
|
return { output: wrappedContent };
|
|
578
644
|
}
|
|
579
645
|
catch (err) {
|
|
@@ -581,6 +647,15 @@ export async function readFile(workspaceRoot, filePath, startLine, endLine, targ
|
|
|
581
647
|
return { output: '', error: `Failed to read file "${filePath}": ${message}` };
|
|
582
648
|
}
|
|
583
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Creates a new file or completely overwrites an existing file with the provided content,
|
|
652
|
+
* performing local syntax validation and iterative AI syntax auto-correction if validation fails.
|
|
653
|
+
*
|
|
654
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
655
|
+
* @param filePath - Relative path to the target file.
|
|
656
|
+
* @param content - The complete content to write.
|
|
657
|
+
* @returns A promise resolving to a {@link ToolResult} indicating success or failure.
|
|
658
|
+
*/
|
|
584
659
|
export async function writeFile(workspaceRoot, filePath, content) {
|
|
585
660
|
try {
|
|
586
661
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
@@ -640,6 +715,13 @@ export async function writeFile(workspaceRoot, filePath, content) {
|
|
|
640
715
|
};
|
|
641
716
|
}
|
|
642
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Deletes a file from the filesystem and records the deletion in the change logger.
|
|
720
|
+
*
|
|
721
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
722
|
+
* @param filePath - Relative path to the file to delete.
|
|
723
|
+
* @returns A promise resolving to a {@link ToolResult} indicating success or failure.
|
|
724
|
+
*/
|
|
643
725
|
export async function deleteFile(workspaceRoot, filePath) {
|
|
644
726
|
try {
|
|
645
727
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
@@ -659,6 +741,14 @@ export async function deleteFile(workspaceRoot, filePath) {
|
|
|
659
741
|
return { output: '', error: `Failed to delete file "${filePath}": ${message}` };
|
|
660
742
|
}
|
|
661
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Moves or renames a file from `sourcePath` to `targetPath`.
|
|
746
|
+
*
|
|
747
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
748
|
+
* @param sourcePath - Original relative file path.
|
|
749
|
+
* @param targetPath - New destination relative file path.
|
|
750
|
+
* @returns A promise resolving to a {@link ToolResult} indicating success or failure.
|
|
751
|
+
*/
|
|
662
752
|
export async function renameFile(workspaceRoot, sourcePath, targetPath) {
|
|
663
753
|
try {
|
|
664
754
|
const absSource = resolveAndValidatePath(workspaceRoot, sourcePath);
|
|
@@ -683,6 +773,15 @@ export async function renameFile(workspaceRoot, sourcePath, targetPath) {
|
|
|
683
773
|
return { output: '', error: `Failed to move/rename file: ${message}` };
|
|
684
774
|
}
|
|
685
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Performs one or multiple targeted search-and-replace edits on a file, utilizing deterministic fuzzy matching,
|
|
778
|
+
* fallback AI fuzzy matching, syntax validation, and atomic writes.
|
|
779
|
+
*
|
|
780
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
781
|
+
* @param filePath - Relative path to the target file.
|
|
782
|
+
* @param edits - An array of edit objects containing `searchContent` and `replaceContent`.
|
|
783
|
+
* @returns A promise resolving to a {@link ToolResult} indicating success or failure.
|
|
784
|
+
*/
|
|
686
785
|
export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
687
786
|
const MAX_MODIFY_RETRIES = 4;
|
|
688
787
|
const absPath = resolveAndValidatePath(workspaceRoot, filePath);
|
|
@@ -691,6 +790,7 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
691
790
|
const existing = await fs.readFile(absPath, 'utf-8');
|
|
692
791
|
let modified = existing;
|
|
693
792
|
const strategies = [];
|
|
793
|
+
const appliedEdits = [];
|
|
694
794
|
for (let i = 0; i < edits.length; i++) {
|
|
695
795
|
const edit = edits[i];
|
|
696
796
|
const match = findBestMatch(modified, edit.searchContent);
|
|
@@ -703,15 +803,29 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
703
803
|
modified = existing; // reset
|
|
704
804
|
break;
|
|
705
805
|
}
|
|
806
|
+
// Fall back to AI fuzzy search matching when deterministic findBestMatch fails after retries
|
|
807
|
+
const aiResult = await aiFuzzyMatch(modified, edit.searchContent, edit.replaceContent, filePath);
|
|
808
|
+
if (aiResult.success && aiResult.content) {
|
|
809
|
+
modified = aiResult.content;
|
|
810
|
+
strategies.push(`Edit #${i + 1}: AI Fuzzy Match`);
|
|
811
|
+
appliedEdits.push({ index: i + 1, line: -1, strategy: 'AI Fuzzy Match' });
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
706
814
|
// Provide a preview of the file to help the AI self-correct
|
|
707
815
|
const preview = modified.split('\n').slice(0, 30).join('\n');
|
|
816
|
+
const priorEditsSummary = appliedEdits.length > 0
|
|
817
|
+
? `\nPrior applied edits before failure:\n` + appliedEdits.map((e) => ` - Edit #${e.index}: Line ${e.line === -1 ? 'N/A' : e.line} (${e.strategy})`).join('\n') + '\n'
|
|
818
|
+
: '';
|
|
708
819
|
return {
|
|
709
820
|
output: '',
|
|
710
|
-
error: `Edit #${i + 1} failed: Search content not found in "${filePath}"
|
|
821
|
+
error: `Edit #${i + 1} failed: Search content not found in "${filePath}".${priorEditsSummary}\n` +
|
|
822
|
+
`Actionable guidance: Ensure searchContent contains sufficient unique context (e.g. 5-10 surrounding lines) to match distinctly. Call 'read_file' to examine the latest file contents before retrying.\n\nFile start preview:\n${preview}\n...`,
|
|
711
823
|
};
|
|
712
824
|
}
|
|
825
|
+
const matchLine = modified.slice(0, match.start).split('\n').length;
|
|
713
826
|
modified = applyMatch(modified, match, edit.replaceContent);
|
|
714
|
-
strategies.push(`Edit #${i + 1}: ${match.strategy}`);
|
|
827
|
+
strategies.push(`Edit #${i + 1}: Line ${matchLine} (${match.strategy})`);
|
|
828
|
+
appliedEdits.push({ index: i + 1, line: matchLine, strategy: match.strategy });
|
|
715
829
|
const collector = getMetricCollector();
|
|
716
830
|
if (collector) {
|
|
717
831
|
if (match.strategy.startsWith('Exact Match')) {
|
|
@@ -741,22 +855,29 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
741
855
|
let success = false;
|
|
742
856
|
while (attempts < MAX_RETRIES && !success) {
|
|
743
857
|
attempts++;
|
|
744
|
-
const fixed = await validateAndFixSyntax(finalModified, filePath);
|
|
858
|
+
const fixed = await validateAndFixSyntax(finalModified, filePath, localResult.error);
|
|
745
859
|
if (fixed) {
|
|
746
860
|
finalModified = fixed;
|
|
747
861
|
const recheck = localValidate(filePath, finalModified);
|
|
748
862
|
if (recheck.isValid) {
|
|
749
863
|
success = true;
|
|
750
864
|
}
|
|
865
|
+
else {
|
|
866
|
+
localResult = recheck;
|
|
867
|
+
}
|
|
751
868
|
}
|
|
752
869
|
else {
|
|
753
870
|
break;
|
|
754
871
|
}
|
|
755
872
|
}
|
|
756
873
|
if (!success) {
|
|
874
|
+
const appliedSummary = appliedEdits.length > 0
|
|
875
|
+
? `\nApplied edit details:\n` + appliedEdits.map((e) => ` - Edit #${e.index}: Line ${e.line === -1 ? 'N/A' : e.line} (${e.strategy})`).join('\n')
|
|
876
|
+
: '';
|
|
757
877
|
return {
|
|
758
878
|
output: '',
|
|
759
|
-
error: `Syntax validation failed after ${attempts} attempts: ${localResult.error || 'Unknown syntax error'}
|
|
879
|
+
error: `Syntax validation failed after ${attempts} attempts: ${localResult.error || 'Unknown syntax error'}${appliedSummary}\n` +
|
|
880
|
+
`Actionable guidance: A search block may have matched at an incorrect line due to non-unique search context in a large file. Re-examine the target file with 'read_file' and provide a searchContent block with more unique surrounding lines (e.g. 5-10 lines of distinct context).`,
|
|
760
881
|
};
|
|
761
882
|
}
|
|
762
883
|
}
|
|
@@ -779,6 +900,15 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
|
|
|
779
900
|
}
|
|
780
901
|
return { output: '', error: 'Modify failed.' };
|
|
781
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Lists files and subdirectories within a directory, returning a recursive ASCII tree structure
|
|
905
|
+
* while respecting ignore rules and excluded extensions.
|
|
906
|
+
*
|
|
907
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
908
|
+
* @param dirPath - Relative path to the target directory.
|
|
909
|
+
* @param maxDepth - Maximum recursion depth (defaults to 3).
|
|
910
|
+
* @returns A promise resolving to a {@link ToolResult} containing the formatted tree output.
|
|
911
|
+
*/
|
|
782
912
|
export async function listDirectory(workspaceRoot, dirPath, maxDepth = 3) {
|
|
783
913
|
try {
|
|
784
914
|
const absPath = resolveAndValidatePath(workspaceRoot, dirPath);
|
|
@@ -833,11 +963,19 @@ export async function listDirectory(workspaceRoot, dirPath, maxDepth = 3) {
|
|
|
833
963
|
};
|
|
834
964
|
}
|
|
835
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Executes a shell command within the workspace root with a timeout and buffer limit.
|
|
968
|
+
*
|
|
969
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
970
|
+
* @param command - The shell command string to execute.
|
|
971
|
+
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
972
|
+
* @returns A promise resolving to a {@link ToolResult} containing stdout/stderr or an error.
|
|
973
|
+
*/
|
|
836
974
|
export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
837
975
|
try {
|
|
838
976
|
const { stdout, stderr } = await execAsync(command, {
|
|
839
977
|
cwd: workspaceRoot,
|
|
840
|
-
timeout: 120_000, // 120
|
|
978
|
+
timeout: 120_000, // 120 second timeout
|
|
841
979
|
maxBuffer: 1024 * 1024 * 2, // 2 MB buffer
|
|
842
980
|
signal: abortSignal,
|
|
843
981
|
});
|
|
@@ -862,45 +1000,38 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
|
862
1000
|
return { output: '', error: `Command failed: ${truncatedMsg}` };
|
|
863
1001
|
}
|
|
864
1002
|
}
|
|
865
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Performs a grep text or regular expression search across files in the workspace.
|
|
1005
|
+
*
|
|
1006
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
1007
|
+
* @param pattern - The search text or regex pattern.
|
|
1008
|
+
* @param fileGlob - Optional file glob to restrict matched files.
|
|
1009
|
+
* @param fixedStrings - If true, treats the pattern as a literal fixed string.
|
|
1010
|
+
* @param dirPath - Optional subdirectory path to restrict the search.
|
|
1011
|
+
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
1012
|
+
* @returns A promise resolving to a {@link ToolResult} containing matching lines and file paths.
|
|
1013
|
+
*/
|
|
1014
|
+
export async function grepSearch(workspaceRoot, pattern, fileGlob, fixedStrings, dirPath, abortSignal) {
|
|
866
1015
|
try {
|
|
867
|
-
const {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
// We use grep -rnIEi for recursive, line numbers, skip binary, extended regex, case-insensitive
|
|
878
|
-
const globArg = fileGlob ? ` --include="${fileGlob}"` : '';
|
|
879
|
-
const cmd = `grep -rnIEi --color=never ${excludeDirArgs} ${excludeFileArgs}${globArg} "${pattern.replace(/"/g, '\\"')}" .`;
|
|
880
|
-
const { stdout } = await execAsync(cmd, {
|
|
881
|
-
cwd: workspaceRoot,
|
|
882
|
-
timeout: 15_000,
|
|
883
|
-
maxBuffer: 1024 * 1024 * 2,
|
|
884
|
-
signal: abortSignal,
|
|
885
|
-
});
|
|
886
|
-
// Limit output to 50 results
|
|
887
|
-
const lines = stdout.trim().split('\n');
|
|
888
|
-
const limited = lines.slice(0, 50);
|
|
889
|
-
const resultText = limited.join('\n') + (lines.length > 50 ? `\n\n... (${lines.length - 50} more results truncated)` : '');
|
|
890
|
-
if (!resultText)
|
|
891
|
-
return { output: `No matches found for "${pattern}".` };
|
|
892
|
-
const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
|
|
893
|
-
return { output: wrappedResult };
|
|
1016
|
+
const { ig } = await getIgnoredPaths(workspaceRoot);
|
|
1017
|
+
// Node.js cross-platform implementation
|
|
1018
|
+
// This avoids shell injection risks and correctly supports path globs via 'ignore' package.
|
|
1019
|
+
const finalPattern = fixedStrings ? pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : pattern;
|
|
1020
|
+
let regexPattern;
|
|
1021
|
+
try {
|
|
1022
|
+
regexPattern = new RegExp(finalPattern, 'gi');
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
return { output: `Invalid search pattern: ${err.message}` };
|
|
894
1026
|
}
|
|
895
|
-
//
|
|
896
|
-
const
|
|
897
|
-
const globPattern = fileGlob
|
|
898
|
-
? new RegExp('^' + fileGlob.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i')
|
|
899
|
-
: null;
|
|
1027
|
+
// Use ignore package to evaluate path globs (e.g. src/**/*.ts)
|
|
1028
|
+
const globMatcher = fileGlob ? ignore().add(fileGlob) : null;
|
|
900
1029
|
const results = [];
|
|
901
1030
|
async function walk(dir) {
|
|
902
1031
|
if (results.length >= 50)
|
|
903
1032
|
return;
|
|
1033
|
+
if (abortSignal?.aborted)
|
|
1034
|
+
return;
|
|
904
1035
|
let entries;
|
|
905
1036
|
try {
|
|
906
1037
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -911,11 +1042,14 @@ export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal)
|
|
|
911
1042
|
for (const entry of entries) {
|
|
912
1043
|
if (results.length >= 50)
|
|
913
1044
|
return;
|
|
1045
|
+
if (abortSignal?.aborted)
|
|
1046
|
+
return;
|
|
914
1047
|
if (entry.name.startsWith('.'))
|
|
915
1048
|
continue;
|
|
916
1049
|
const entryPath = path.join(dir, entry.name);
|
|
917
1050
|
const relPath = path.relative(workspaceRoot, entryPath).replace(/\\/g, '/');
|
|
918
1051
|
if (entry.isDirectory()) {
|
|
1052
|
+
// Check if directory is ignored by .gitignore
|
|
919
1053
|
if (ig.ignores(relPath + '/'))
|
|
920
1054
|
continue;
|
|
921
1055
|
await walk(entryPath);
|
|
@@ -925,7 +1059,8 @@ export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal)
|
|
|
925
1059
|
continue;
|
|
926
1060
|
if (EXCLUDED_EXTENSIONS.some((ext) => entry.name.endsWith(ext.replace('*', ''))))
|
|
927
1061
|
continue;
|
|
928
|
-
|
|
1062
|
+
// If a fileGlob was provided, verify this path matches the glob (i.e. 'ignore' would ignore it)
|
|
1063
|
+
if (globMatcher && !globMatcher.ignores(relPath))
|
|
929
1064
|
continue;
|
|
930
1065
|
try {
|
|
931
1066
|
const filePath = path.join(dir, entry.name);
|
|
@@ -936,8 +1071,8 @@ export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal)
|
|
|
936
1071
|
break;
|
|
937
1072
|
regexPattern.lastIndex = 0;
|
|
938
1073
|
if (regexPattern.test(lines[i])) {
|
|
939
|
-
const
|
|
940
|
-
results.push(`${
|
|
1074
|
+
const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
|
|
1075
|
+
results.push(`${relativePath}:${i + 1}:${lines[i]}`);
|
|
941
1076
|
}
|
|
942
1077
|
}
|
|
943
1078
|
}
|
|
@@ -947,36 +1082,38 @@ export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal)
|
|
|
947
1082
|
}
|
|
948
1083
|
}
|
|
949
1084
|
}
|
|
950
|
-
|
|
1085
|
+
const searchRoot = dirPath ? path.resolve(workspaceRoot, dirPath) : workspaceRoot;
|
|
1086
|
+
await walk(searchRoot);
|
|
1087
|
+
if (abortSignal?.aborted) {
|
|
1088
|
+
return { output: '', error: 'Grep search aborted by user.' };
|
|
1089
|
+
}
|
|
951
1090
|
if (results.length === 0)
|
|
952
1091
|
return { output: `No matches found for "${pattern}".` };
|
|
953
1092
|
const resultText = results.join('\n') + (results.length >= 50 ? `\n\n... (more results truncated)` : '');
|
|
954
|
-
const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
|
|
1093
|
+
const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
955
1094
|
return { output: wrappedResult };
|
|
956
1095
|
}
|
|
957
1096
|
catch (err) {
|
|
958
|
-
if (err instanceof Error && 'code' in err && err.code === 1) {
|
|
959
|
-
return { output: `No matches found for "${pattern}".` };
|
|
960
|
-
}
|
|
961
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
962
|
-
return { output: '', error: 'Grep search aborted by user.' };
|
|
963
|
-
}
|
|
964
1097
|
const message = err instanceof Error ? err.message : String(err);
|
|
965
|
-
|
|
966
|
-
if (message.includes('Command failed') &&
|
|
967
|
-
(message.includes('exit code 1') || message.includes('exited with code 1'))) {
|
|
968
|
-
return { output: `No matches found for "${pattern}".` };
|
|
969
|
-
}
|
|
970
|
-
return { output: '', error: `Grep failed: ${message}` };
|
|
1098
|
+
return { output: '', error: `Grep search failed: ${message}` };
|
|
971
1099
|
}
|
|
972
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Traces file dependencies (forward imports and reverse dependants) to determine the blast radius of changes.
|
|
1103
|
+
*
|
|
1104
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
1105
|
+
* @param filePath - Relative path to the file to trace.
|
|
1106
|
+
* @param direction - Trace direction (`'both'`, `'forward'`, or `'reverse'`).
|
|
1107
|
+
* @param maxDepth - Maximum recursion depth (defaults to 3).
|
|
1108
|
+
* @returns A promise resolving to a {@link ToolResult} containing formatted dependency results.
|
|
1109
|
+
*/
|
|
973
1110
|
export async function traceDependencies(workspaceRoot, filePath, direction, maxDepth) {
|
|
974
1111
|
try {
|
|
975
1112
|
const dir = direction === 'forward' || direction === 'reverse' ? direction : 'both';
|
|
976
1113
|
const depth = maxDepth && maxDepth > 0 ? Math.min(maxDepth, 5) : 3;
|
|
977
1114
|
const result = await findDependencies(workspaceRoot, filePath, dir, depth);
|
|
978
1115
|
const formatted = formatDependencyResult(result);
|
|
979
|
-
const wrappedResult = `<workspace_file path="dependency_trace_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(formatted)}\n]]></content_data>\n</workspace_file>`;
|
|
1116
|
+
const wrappedResult = `<workspace_file path="dependency_trace_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(formatted)}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
980
1117
|
return { output: wrappedResult };
|
|
981
1118
|
}
|
|
982
1119
|
catch (err) {
|
|
@@ -984,6 +1121,15 @@ export async function traceDependencies(workspaceRoot, filePath, direction, maxD
|
|
|
984
1121
|
return { output: '', error: `Dependency tracing failed for "${filePath}": ${message}` };
|
|
985
1122
|
}
|
|
986
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Finds files modified within the workspace recently (within a specified time window).
|
|
1126
|
+
*
|
|
1127
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
1128
|
+
* @param dirPath - Subdirectory path to search from (defaults to `'.'`).
|
|
1129
|
+
* @param minutes - Time window in minutes (defaults to 60).
|
|
1130
|
+
* @param maxDepth - Maximum directory traversal depth (defaults to 5).
|
|
1131
|
+
* @returns A promise resolving to a {@link ToolResult} containing recently modified files.
|
|
1132
|
+
*/
|
|
987
1133
|
export async function findRecentChanges(workspaceRoot, dirPath = '.', minutes = 60, maxDepth = 5) {
|
|
988
1134
|
try {
|
|
989
1135
|
const absPath = resolveAndValidatePath(workspaceRoot, dirPath);
|
|
@@ -1041,7 +1187,7 @@ export async function findRecentChanges(workspaceRoot, dirPath = '.', minutes =
|
|
|
1041
1187
|
return `- ${f.path} (${minsAgo} minutes ago)`;
|
|
1042
1188
|
});
|
|
1043
1189
|
const resultText = `Files modified in the last ${minutes} minutes:\n${lines.join('\n')}`;
|
|
1044
|
-
const wrappedResult = `<workspace_file path="recent_changes">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
|
|
1190
|
+
const wrappedResult = `<workspace_file path="recent_changes">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
1045
1191
|
return { output: wrappedResult };
|
|
1046
1192
|
}
|
|
1047
1193
|
catch (err) {
|
|
@@ -1053,6 +1199,16 @@ export async function findRecentChanges(workspaceRoot, dirPath = '.', minutes =
|
|
|
1053
1199
|
}
|
|
1054
1200
|
}
|
|
1055
1201
|
// ─── Tool Dispatcher ─────────────────────────────────────────────────
|
|
1202
|
+
/**
|
|
1203
|
+
* Writes a disposable scratchpad script to a temporary file, executes it using the specified runtime,
|
|
1204
|
+
* and returns standard output and standard error.
|
|
1205
|
+
*
|
|
1206
|
+
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
1207
|
+
* @param language - Runtime language (`'node'`, `'ts-node'`, `'python'`, `'bash'`, `'go'`, or `'rust'`).
|
|
1208
|
+
* @param code - The exact script source code to execute.
|
|
1209
|
+
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
1210
|
+
* @returns A promise resolving to a {@link ToolResult} containing execution output.
|
|
1211
|
+
*/
|
|
1056
1212
|
export async function runDebugScript(workspaceRoot, language, code, abortSignal) {
|
|
1057
1213
|
const extMap = {
|
|
1058
1214
|
node: '.js',
|
|
@@ -1144,6 +1300,11 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
|
|
|
1144
1300
|
/**
|
|
1145
1301
|
* Resolves `@alias/` prefixed paths in tool arguments to the correct workspace root
|
|
1146
1302
|
* and relative path. Returns the effective workspaceRoot and the cleaned arguments.
|
|
1303
|
+
*
|
|
1304
|
+
* @param primaryRoot - Absolute path to the primary workspace root.
|
|
1305
|
+
* @param toolName - Name of the tool being executed.
|
|
1306
|
+
* @param args - Tool arguments object.
|
|
1307
|
+
* @returns An object containing `effectiveRoot` and `resolvedArgs`.
|
|
1147
1308
|
*/
|
|
1148
1309
|
function resolveWorkspaceArgs(primaryRoot, toolName, args) {
|
|
1149
1310
|
// Only resolve if workspaces are registered
|
|
@@ -1183,12 +1344,20 @@ function resolveWorkspaceArgs(primaryRoot, toolName, args) {
|
|
|
1183
1344
|
/**
|
|
1184
1345
|
* Runs grep across all registered workspaces (primary + external) and merges results.
|
|
1185
1346
|
* Results from external workspaces are prefixed with @alias/ for disambiguation.
|
|
1347
|
+
*
|
|
1348
|
+
* @param primaryRoot - Absolute path to the primary workspace root.
|
|
1349
|
+
* @param pattern - The search text or regex pattern.
|
|
1350
|
+
* @param fileGlob - Optional file glob restriction.
|
|
1351
|
+
* @param fixedStrings - If true, treats the pattern as a literal fixed string.
|
|
1352
|
+
* @param dirPath - Optional subdirectory restriction.
|
|
1353
|
+
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
1354
|
+
* @returns A promise resolving to a {@link ToolResult} containing merged search results.
|
|
1186
1355
|
*/
|
|
1187
|
-
async function crossWorkspaceGrep(primaryRoot, pattern, fileGlob, abortSignal) {
|
|
1356
|
+
async function crossWorkspaceGrep(primaryRoot, pattern, fileGlob, fixedStrings, dirPath, abortSignal) {
|
|
1188
1357
|
const allRoots = workspaceRegistry.getAllRoots(primaryRoot);
|
|
1189
1358
|
const allResults = [];
|
|
1190
1359
|
for (const { alias, root } of allRoots) {
|
|
1191
|
-
const result = await grepSearch(root, pattern, fileGlob, abortSignal);
|
|
1360
|
+
const result = await grepSearch(root, pattern, fileGlob, fixedStrings, dirPath, abortSignal);
|
|
1192
1361
|
if (result.error)
|
|
1193
1362
|
continue;
|
|
1194
1363
|
// Extract the raw text from the XML wrapper
|
|
@@ -1214,10 +1383,20 @@ async function crossWorkspaceGrep(primaryRoot, pattern, fileGlob, abortSignal) {
|
|
|
1214
1383
|
}
|
|
1215
1384
|
const limited = allResults.slice(0, 80);
|
|
1216
1385
|
const resultText = limited.join('\n') + (allResults.length > 80 ? `\n\n... (${allResults.length - 80} more results truncated)` : '');
|
|
1217
|
-
const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
|
|
1386
|
+
const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
1218
1387
|
return { output: wrappedResult };
|
|
1219
1388
|
}
|
|
1220
1389
|
const currentTasksByAgent = new Map();
|
|
1390
|
+
/**
|
|
1391
|
+
* Central tool dispatcher that resolves multi-workspace paths, executes the requested tool
|
|
1392
|
+
* with the provided arguments, records metrics, and returns the standardized {@link ToolResult}.
|
|
1393
|
+
*
|
|
1394
|
+
* @param workspaceRoot - Absolute path to the primary workspace root directory.
|
|
1395
|
+
* @param toolName - Name of the tool to invoke.
|
|
1396
|
+
* @param args - Key-value map of tool arguments.
|
|
1397
|
+
* @param abortSignal - Optional AbortSignal to cancel tool execution.
|
|
1398
|
+
* @returns A promise resolving to a {@link ToolResult}.
|
|
1399
|
+
*/
|
|
1221
1400
|
export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
|
|
1222
1401
|
// ─── Multi-Workspace Path Resolution ─────────────────────────────
|
|
1223
1402
|
// Intercept @alias/ prefixed paths and swap workspaceRoot + relative path
|
|
@@ -1225,6 +1404,19 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
|
|
|
1225
1404
|
const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, toolName, args);
|
|
1226
1405
|
let result;
|
|
1227
1406
|
switch (toolName) {
|
|
1407
|
+
case 'perform_web_search': {
|
|
1408
|
+
const { createWebSearchAgentSession } = await import('./ai.js');
|
|
1409
|
+
const webSession = createWebSearchAgentSession();
|
|
1410
|
+
try {
|
|
1411
|
+
const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${resolvedArgs.query}"`, undefined, abortSignal);
|
|
1412
|
+
const searchSummary = webResult.response.text()?.trim() || 'No relevant information found.';
|
|
1413
|
+
result = { output: `Web Search Findings:\n${searchSummary}` };
|
|
1414
|
+
}
|
|
1415
|
+
catch (e) {
|
|
1416
|
+
result = { error: e.message || 'Failed to search the web', output: '' };
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1228
1420
|
case 'read_file':
|
|
1229
1421
|
result = await readFile(effectiveRoot, resolvedArgs.filePath, resolvedArgs.startLine, resolvedArgs.endLine, resolvedArgs.targetElements);
|
|
1230
1422
|
break;
|
|
@@ -1317,15 +1509,15 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
|
|
|
1317
1509
|
const wsParam = resolvedArgs.workspace;
|
|
1318
1510
|
if (wsParam === 'all') {
|
|
1319
1511
|
// Cross-workspace search across all registered workspaces
|
|
1320
|
-
result = await crossWorkspaceGrep(workspaceRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
|
|
1512
|
+
result = await crossWorkspaceGrep(workspaceRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, resolvedArgs.fixedStrings, resolvedArgs.dirPath, abortSignal);
|
|
1321
1513
|
}
|
|
1322
1514
|
else if (wsParam && workspaceRegistry.get(wsParam)) {
|
|
1323
1515
|
// Search in a specific external workspace
|
|
1324
1516
|
const ws = workspaceRegistry.get(wsParam);
|
|
1325
|
-
result = await grepSearch(ws.absolutePath, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
|
|
1517
|
+
result = await grepSearch(ws.absolutePath, resolvedArgs.pattern, resolvedArgs.fileGlob, resolvedArgs.fixedStrings, resolvedArgs.dirPath, abortSignal);
|
|
1326
1518
|
}
|
|
1327
1519
|
else {
|
|
1328
|
-
result = await grepSearch(effectiveRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
|
|
1520
|
+
result = await grepSearch(effectiveRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, resolvedArgs.fixedStrings, resolvedArgs.dirPath, abortSignal);
|
|
1329
1521
|
}
|
|
1330
1522
|
break;
|
|
1331
1523
|
}
|