@xagent-ai/cli 1.2.0 → 1.2.2
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 +1 -1
- package/README_CN.md +1 -1
- package/dist/agents.js +164 -164
- package/dist/agents.js.map +1 -1
- package/dist/ai-client.d.ts +4 -6
- package/dist/ai-client.d.ts.map +1 -1
- package/dist/ai-client.js +137 -115
- package/dist/ai-client.js.map +1 -1
- package/dist/auth.js +4 -4
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +184 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/context-compressor.d.ts.map +1 -1
- package/dist/context-compressor.js +65 -81
- package/dist/context-compressor.js.map +1 -1
- package/dist/conversation.d.ts +1 -1
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +5 -31
- package/dist/conversation.js.map +1 -1
- package/dist/memory.d.ts +5 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +77 -37
- package/dist/memory.js.map +1 -1
- package/dist/remote-ai-client.d.ts +1 -8
- package/dist/remote-ai-client.d.ts.map +1 -1
- package/dist/remote-ai-client.js +55 -65
- package/dist/remote-ai-client.js.map +1 -1
- package/dist/retry.d.ts +35 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +166 -0
- package/dist/retry.js.map +1 -0
- package/dist/session.d.ts +0 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +243 -312
- package/dist/session.js.map +1 -1
- package/dist/slash-commands.d.ts +1 -0
- package/dist/slash-commands.d.ts.map +1 -1
- package/dist/slash-commands.js +91 -9
- package/dist/slash-commands.js.map +1 -1
- package/dist/smart-approval.d.ts.map +1 -1
- package/dist/smart-approval.js +18 -17
- package/dist/smart-approval.js.map +1 -1
- package/dist/system-prompt-generator.d.ts.map +1 -1
- package/dist/system-prompt-generator.js +149 -139
- package/dist/system-prompt-generator.js.map +1 -1
- package/dist/theme.d.ts +48 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +254 -0
- package/dist/theme.js.map +1 -1
- package/dist/tools/edit-diff.d.ts +32 -0
- package/dist/tools/edit-diff.d.ts.map +1 -0
- package/dist/tools/edit-diff.js +185 -0
- package/dist/tools/edit-diff.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +129 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools.d.ts +19 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +979 -631
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +6 -31
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/agents.ts +504 -504
- package/src/ai-client.ts +1559 -1458
- package/src/auth.ts +4 -4
- package/src/cli.ts +195 -1
- package/src/config.ts +3 -3
- package/src/memory.ts +55 -14
- package/src/remote-ai-client.ts +663 -683
- package/src/retry.ts +217 -0
- package/src/session.ts +1736 -1840
- package/src/slash-commands.ts +98 -9
- package/src/smart-approval.ts +626 -625
- package/src/system-prompt-generator.ts +853 -843
- package/src/theme.ts +284 -0
- package/src/tools.ts +390 -70
package/src/tools.ts
CHANGED
|
@@ -123,7 +123,7 @@ export class WriteTool implements Tool {
|
|
|
123
123
|
- When user explicitly asks to "create", "write", or "generate" a file
|
|
124
124
|
|
|
125
125
|
# When NOT to Use
|
|
126
|
-
- For making small edits to existing files (use
|
|
126
|
+
- For making small edits to existing files (use edit instead)
|
|
127
127
|
- When you only need to append content (read file first, then write)
|
|
128
128
|
- For creating directories (use CreateDirectory instead)
|
|
129
129
|
|
|
@@ -139,22 +139,29 @@ export class WriteTool implements Tool {
|
|
|
139
139
|
- Parent directories are created automatically
|
|
140
140
|
- Use appropriate file extensions
|
|
141
141
|
- Ensure content is complete and syntactically correct
|
|
142
|
-
- For partial edits, use
|
|
142
|
+
- For partial edits, use Edit tool instead`;
|
|
143
143
|
allowedModes = [ExecutionMode.YOLO, ExecutionMode.ACCEPT_EDITS, ExecutionMode.SMART];
|
|
144
144
|
|
|
145
|
-
async execute(params: { filePath: string; content: string }): Promise<{ success: boolean; message: string }> {
|
|
145
|
+
async execute(params: { filePath: string; content: string }): Promise<{ success: boolean; message: string; filePath: string; lineCount: number; preview?: string }> {
|
|
146
146
|
const { filePath, content } = params;
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
try {
|
|
149
149
|
const absolutePath = path.resolve(filePath);
|
|
150
150
|
const dir = path.dirname(absolutePath);
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
await fs.mkdir(dir, { recursive: true });
|
|
153
153
|
await fs.writeFile(absolutePath, content, 'utf-8');
|
|
154
|
-
|
|
154
|
+
|
|
155
|
+
const lineCount = content.split('\n').length;
|
|
156
|
+
const preview = content.split('\n').slice(0, 10).join('\n');
|
|
157
|
+
const isTruncated = lineCount > 10;
|
|
158
|
+
|
|
155
159
|
return {
|
|
156
160
|
success: true,
|
|
157
|
-
message: `Successfully wrote to ${filePath}
|
|
161
|
+
message: `Successfully wrote to ${filePath}`,
|
|
162
|
+
filePath,
|
|
163
|
+
lineCount,
|
|
164
|
+
preview: isTruncated ? preview + '\n...' : preview
|
|
158
165
|
};
|
|
159
166
|
} catch (error: any) {
|
|
160
167
|
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
|
|
@@ -174,9 +181,9 @@ export class GrepTool implements Tool {
|
|
|
174
181
|
- When you need line-by-line results with context
|
|
175
182
|
|
|
176
183
|
# When NOT to Use
|
|
177
|
-
- When you only need to find files containing text (use
|
|
178
|
-
- When searching by file pattern rather than content (use
|
|
179
|
-
- For very large codebases where you only need file names (
|
|
184
|
+
- When you only need to find files containing text (use SearchFiles instead)
|
|
185
|
+
- When searching by file pattern rather than content (use SearchFiles)
|
|
186
|
+
- For very large codebases where you only need file names (SearchFiles is faster)
|
|
180
187
|
|
|
181
188
|
# Parameters
|
|
182
189
|
- \`pattern\`: Regex or literal string to search for
|
|
@@ -316,9 +323,9 @@ export class BashTool implements Tool {
|
|
|
316
323
|
- Any command-line operations
|
|
317
324
|
|
|
318
325
|
# When NOT to Use
|
|
319
|
-
- For file operations (use Read/Write/
|
|
326
|
+
- For file operations (use Read/Write/Edit/CreateDirectory instead)
|
|
320
327
|
- For searching file content (use Grep instead)
|
|
321
|
-
- For finding files (use
|
|
328
|
+
- For finding files (use SearchFiles or ListDirectory instead)
|
|
322
329
|
- For commands that require user interaction (non-interactive only)
|
|
323
330
|
- For dangerous commands without understanding the impact
|
|
324
331
|
|
|
@@ -458,7 +465,7 @@ export class ListDirectoryTool implements Tool {
|
|
|
458
465
|
# When NOT to Use
|
|
459
466
|
- When you need to read file contents (use Read instead)
|
|
460
467
|
- For recursive exploration of entire codebase (use recursive=true)
|
|
461
|
-
- When you need to search for specific files (use
|
|
468
|
+
- When you need to search for specific files (use SearchFiles instead)
|
|
462
469
|
|
|
463
470
|
# Parameters
|
|
464
471
|
- \`path\`: (Optional) Directory path, default: "."
|
|
@@ -501,8 +508,17 @@ export class ListDirectoryTool implements Tool {
|
|
|
501
508
|
}
|
|
502
509
|
}
|
|
503
510
|
|
|
504
|
-
export
|
|
505
|
-
|
|
511
|
+
export interface SearchFilesResult {
|
|
512
|
+
/** Matching file paths relative to search directory */
|
|
513
|
+
files: string[];
|
|
514
|
+
/** Total number of matches found (before limiting) */
|
|
515
|
+
total: number;
|
|
516
|
+
/** Whether results were truncated due to limit */
|
|
517
|
+
truncated: boolean;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export class SearchFilesTool implements Tool {
|
|
521
|
+
name = 'SearchFiles';
|
|
506
522
|
description = `Search for files matching a glob pattern. This is your PRIMARY tool for finding files by name or extension.
|
|
507
523
|
|
|
508
524
|
# When to Use
|
|
@@ -519,11 +535,13 @@ export class SearchCodebaseTool implements Tool {
|
|
|
519
535
|
# Parameters
|
|
520
536
|
- \`pattern\`: Glob pattern (e.g., "**/*.ts", "src/**/*.test.ts")
|
|
521
537
|
- \`path\`: (Optional) Directory to search in, default: "."
|
|
538
|
+
- \`limit\`: (Optional) Maximum number of results to return, default: 1000
|
|
522
539
|
|
|
523
540
|
# Examples
|
|
524
|
-
- Find all TypeScript files:
|
|
525
|
-
- Find test files:
|
|
526
|
-
- Find config files:
|
|
541
|
+
- Find all TypeScript files: SearchFiles(pattern="**/*.ts")
|
|
542
|
+
- Find test files: SearchFiles(pattern="**/*.test.ts")
|
|
543
|
+
- Find config files: SearchFiles(pattern="**/config.*")
|
|
544
|
+
- Limit results: SearchFiles(pattern="**/*.ts", limit=100)
|
|
527
545
|
|
|
528
546
|
# Glob Patterns
|
|
529
547
|
- \`*\` matches any characters except /
|
|
@@ -534,19 +552,28 @@ export class SearchCodebaseTool implements Tool {
|
|
|
534
552
|
# Best Practices
|
|
535
553
|
- Use **/*.ts for recursive search in all directories
|
|
536
554
|
- Combine with path parameter to search specific directories
|
|
555
|
+
- Use limit parameter to avoid huge result sets
|
|
537
556
|
- Results are file paths, not content (use Grep on results if needed)`;
|
|
538
557
|
allowedModes = [ExecutionMode.YOLO, ExecutionMode.ACCEPT_EDITS, ExecutionMode.PLAN, ExecutionMode.SMART];
|
|
539
558
|
|
|
540
|
-
async execute(params: { pattern: string; path?: string }): Promise<
|
|
541
|
-
const { pattern, path: searchPath = '.' } = params;
|
|
542
|
-
|
|
559
|
+
async execute(params: { pattern: string; path?: string; limit?: number }): Promise<SearchFilesResult> {
|
|
560
|
+
const { pattern, path: searchPath = '.', limit = 1000 } = params;
|
|
561
|
+
|
|
543
562
|
try {
|
|
544
563
|
const files = await glob(pattern, {
|
|
545
564
|
cwd: searchPath,
|
|
546
565
|
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**']
|
|
547
566
|
});
|
|
548
567
|
|
|
549
|
-
|
|
568
|
+
const total = files.length;
|
|
569
|
+
const truncated = total > limit;
|
|
570
|
+
const result = truncated ? files.slice(0, limit) : files;
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
files: result,
|
|
574
|
+
total,
|
|
575
|
+
truncated
|
|
576
|
+
};
|
|
550
577
|
} catch (error: any) {
|
|
551
578
|
throw new Error(`Search failed: ${error.message}`);
|
|
552
579
|
}
|
|
@@ -581,16 +608,17 @@ export class DeleteFileTool implements Tool {
|
|
|
581
608
|
- This action is irreversible - be certain before executing`;
|
|
582
609
|
allowedModes = [ExecutionMode.YOLO, ExecutionMode.ACCEPT_EDITS, ExecutionMode.SMART];
|
|
583
610
|
|
|
584
|
-
async execute(params: { filePath: string }): Promise<{ success: boolean; message: string }> {
|
|
611
|
+
async execute(params: { filePath: string }): Promise<{ success: boolean; message: string; filePath: string }> {
|
|
585
612
|
const { filePath } = params;
|
|
586
|
-
|
|
613
|
+
|
|
587
614
|
try {
|
|
588
615
|
const absolutePath = path.resolve(filePath);
|
|
589
616
|
await fs.unlink(absolutePath);
|
|
590
|
-
|
|
617
|
+
|
|
591
618
|
return {
|
|
592
619
|
success: true,
|
|
593
|
-
message: `Successfully deleted ${filePath}
|
|
620
|
+
message: `Successfully deleted ${filePath}`,
|
|
621
|
+
filePath
|
|
594
622
|
};
|
|
595
623
|
} catch (error: any) {
|
|
596
624
|
throw new Error(`Failed to delete file ${filePath}: ${error.message}`);
|
|
@@ -643,9 +671,171 @@ export class CreateDirectoryTool implements Tool {
|
|
|
643
671
|
}
|
|
644
672
|
}
|
|
645
673
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
674
|
+
// 编辑工具辅助函数
|
|
675
|
+
function detectLineEnding(content: string): "\r\n" | "\n" {
|
|
676
|
+
const crlfIdx = content.indexOf("\r\n");
|
|
677
|
+
const lfIdx = content.indexOf("\n");
|
|
678
|
+
if (lfIdx === -1) return "\n";
|
|
679
|
+
if (crlfIdx === -1) return "\n";
|
|
680
|
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function normalizeToLF(text: string): string {
|
|
684
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
|
|
688
|
+
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function normalizeForFuzzyMatch(text: string): string {
|
|
692
|
+
return (
|
|
693
|
+
text
|
|
694
|
+
.split("\n")
|
|
695
|
+
.map((line) => line.trimEnd())
|
|
696
|
+
.join("\n")
|
|
697
|
+
.replace(/['‘’""]/g, "'")
|
|
698
|
+
.replace(/["""]/g, '"')
|
|
699
|
+
.replace(/[—–‑−]/g, "-")
|
|
700
|
+
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ")
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
interface FuzzyMatchResult {
|
|
705
|
+
found: boolean;
|
|
706
|
+
index: number;
|
|
707
|
+
matchLength: number;
|
|
708
|
+
usedFuzzyMatch: boolean;
|
|
709
|
+
contentForReplacement: string;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {
|
|
713
|
+
const exactIndex = content.indexOf(oldText);
|
|
714
|
+
if (exactIndex !== -1) {
|
|
715
|
+
return {
|
|
716
|
+
found: true,
|
|
717
|
+
index: exactIndex,
|
|
718
|
+
matchLength: oldText.length,
|
|
719
|
+
usedFuzzyMatch: false,
|
|
720
|
+
contentForReplacement: content,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
725
|
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
726
|
+
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
727
|
+
|
|
728
|
+
if (fuzzyIndex === -1) {
|
|
729
|
+
return {
|
|
730
|
+
found: false,
|
|
731
|
+
index: -1,
|
|
732
|
+
matchLength: 0,
|
|
733
|
+
usedFuzzyMatch: false,
|
|
734
|
+
contentForReplacement: content,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
found: true,
|
|
740
|
+
index: fuzzyIndex,
|
|
741
|
+
matchLength: fuzzyOldText.length,
|
|
742
|
+
usedFuzzyMatch: true,
|
|
743
|
+
contentForReplacement: fuzzyContent,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function stripBom(content: string): { bom: string; text: string } {
|
|
748
|
+
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function generateDiffString(oldContent: string, newContent: string, contextLines = 4): Promise<{ diff: string; firstChangedLine: number | undefined }> {
|
|
752
|
+
const diffModule = await import("diff");
|
|
753
|
+
const parts = diffModule.diffLines(oldContent, newContent);
|
|
754
|
+
const output: string[] = [];
|
|
755
|
+
|
|
756
|
+
const oldLines = oldContent.split("\n");
|
|
757
|
+
const newLines = newContent.split("\n");
|
|
758
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
759
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
760
|
+
|
|
761
|
+
let oldLineNum = 1;
|
|
762
|
+
let newLineNum = 1;
|
|
763
|
+
let lastWasChange = false;
|
|
764
|
+
let firstChangedLine: number | undefined;
|
|
765
|
+
|
|
766
|
+
for (let i = 0; i < parts.length; i++) {
|
|
767
|
+
const part = parts[i];
|
|
768
|
+
const raw = part.value.split("\n");
|
|
769
|
+
if (raw[raw.length - 1] === "") {
|
|
770
|
+
raw.pop();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (part.added || part.removed) {
|
|
774
|
+
if (firstChangedLine === undefined) {
|
|
775
|
+
firstChangedLine = newLineNum;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
for (const line of raw) {
|
|
779
|
+
if (part.added) {
|
|
780
|
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
|
781
|
+
output.push(`+${lineNum} ${line}`);
|
|
782
|
+
newLineNum++;
|
|
783
|
+
} else {
|
|
784
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
785
|
+
output.push(`-${lineNum} ${line}`);
|
|
786
|
+
oldLineNum++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
lastWasChange = true;
|
|
790
|
+
} else {
|
|
791
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
792
|
+
|
|
793
|
+
if (lastWasChange || nextPartIsChange) {
|
|
794
|
+
let linesToShow = raw;
|
|
795
|
+
let skipStart = 0;
|
|
796
|
+
let skipEnd = 0;
|
|
797
|
+
|
|
798
|
+
if (!lastWasChange) {
|
|
799
|
+
skipStart = Math.max(0, raw.length - contextLines);
|
|
800
|
+
linesToShow = raw.slice(skipStart);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
804
|
+
skipEnd = linesToShow.length - contextLines;
|
|
805
|
+
linesToShow = linesToShow.slice(0, contextLines);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (skipStart > 0) {
|
|
809
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
810
|
+
oldLineNum += skipStart;
|
|
811
|
+
newLineNum += skipStart;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
for (const line of linesToShow) {
|
|
815
|
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
816
|
+
output.push(` ${lineNum} ${line}`);
|
|
817
|
+
oldLineNum++;
|
|
818
|
+
newLineNum++;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (skipEnd > 0) {
|
|
822
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
823
|
+
}
|
|
824
|
+
} else {
|
|
825
|
+
oldLineNum += raw.length;
|
|
826
|
+
newLineNum += raw.length;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
lastWasChange = false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return { diff: output.join("\n"), firstChangedLine };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export class EditTool implements Tool {
|
|
837
|
+
name = 'Edit';
|
|
838
|
+
description = `Edit a file by replacing exact text. This is your PRIMARY tool for making targeted edits to code.
|
|
649
839
|
|
|
650
840
|
# When to Use
|
|
651
841
|
- Modifying specific code sections without rewriting entire files
|
|
@@ -656,32 +846,39 @@ export class ReplaceTool implements Tool {
|
|
|
656
846
|
# When NOT to Use
|
|
657
847
|
- When you need to create a completely new file (use Write instead)
|
|
658
848
|
- When you want to append content to a file (read first, then Write)
|
|
659
|
-
- When making changes across multiple files (use Grep to find, then
|
|
849
|
+
- When making changes across multiple files (use Grep to find, then edit individually)
|
|
660
850
|
|
|
661
851
|
# Parameters
|
|
662
|
-
- \`file_path\`: Path to the file to edit
|
|
852
|
+
- \`file_path\`: Path to the file to edit (relative or absolute)
|
|
663
853
|
- \`instruction\`: Description of what to change (for your own tracking)
|
|
664
854
|
- \`old_string\`: The exact text to find and replace (must match exactly)
|
|
665
855
|
- \`new_string\`: The new text to replace with
|
|
666
856
|
|
|
667
857
|
# Critical Requirements
|
|
668
858
|
- \`old_string\` MUST be an EXACT match, including whitespace and indentation
|
|
669
|
-
- Include at least 3 lines
|
|
670
|
-
-
|
|
859
|
+
- Include sufficient context (at least 3 lines) before and after the target text to ensure unique matching
|
|
860
|
+
- The file must exist before editing
|
|
861
|
+
|
|
862
|
+
# Fuzzy Matching
|
|
863
|
+
This tool supports fuzzy matching to handle minor formatting differences:
|
|
864
|
+
- Trailing whitespace is ignored
|
|
865
|
+
- Smart quotes (', ", , ) are normalized to ASCII
|
|
866
|
+
- Unicode dashes/hyphens are normalized to ASCII hyphen
|
|
867
|
+
- Special Unicode spaces are normalized to regular space
|
|
671
868
|
|
|
672
869
|
# Examples
|
|
673
|
-
|
|
870
|
+
edit(
|
|
674
871
|
file_path="src/app.ts",
|
|
675
872
|
instruction="Update API endpoint",
|
|
676
|
-
old_string="const API_URL = 'https://api.old.com';",
|
|
677
|
-
new_string="const API_URL = 'https://api.new.com';"
|
|
873
|
+
old_string="const API_URL = 'https://api.old.com'\\nconst PORT = 8080;",
|
|
874
|
+
new_string="const API_URL = 'https://api.new.com'\\nconst PORT = 3000;"
|
|
678
875
|
)
|
|
679
876
|
|
|
680
877
|
# Best Practices
|
|
681
878
|
- Read the file first to understand the exact content
|
|
682
879
|
- Include sufficient context in old_string to ensure unique match
|
|
683
|
-
-
|
|
684
|
-
-
|
|
880
|
+
- If fuzzy matching is needed, the tool will automatically apply it
|
|
881
|
+
- Check the diff output to verify the change is correct`;
|
|
685
882
|
allowedModes = [ExecutionMode.YOLO, ExecutionMode.ACCEPT_EDITS, ExecutionMode.SMART];
|
|
686
883
|
|
|
687
884
|
async execute(params: {
|
|
@@ -689,39 +886,89 @@ replace(
|
|
|
689
886
|
instruction: string;
|
|
690
887
|
old_string: string;
|
|
691
888
|
new_string: string;
|
|
692
|
-
}): Promise<{ success: boolean; message: string;
|
|
889
|
+
}): Promise<{ success: boolean; message: string; diff?: string; firstChangedLine?: number }> {
|
|
693
890
|
const { file_path, instruction, old_string, new_string } = params;
|
|
694
891
|
|
|
695
892
|
try {
|
|
696
893
|
const absolutePath = path.resolve(file_path);
|
|
697
|
-
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
698
|
-
|
|
699
|
-
const occurrences = (content.match(new RegExp(this.escapeRegExp(old_string), 'g')) || []).length;
|
|
700
894
|
|
|
701
|
-
|
|
895
|
+
// Check if file exists
|
|
896
|
+
try {
|
|
897
|
+
await fs.access(absolutePath);
|
|
898
|
+
} catch {
|
|
702
899
|
return {
|
|
703
900
|
success: false,
|
|
704
|
-
message: `
|
|
705
|
-
changes: 0
|
|
901
|
+
message: `File not found: ${file_path}`,
|
|
706
902
|
};
|
|
707
903
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
await fs.
|
|
711
|
-
|
|
904
|
+
|
|
905
|
+
// Read the file
|
|
906
|
+
const buffer = await fs.readFile(absolutePath);
|
|
907
|
+
const rawContent = buffer.toString("utf-8");
|
|
908
|
+
|
|
909
|
+
// Strip BOM before matching
|
|
910
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
911
|
+
|
|
912
|
+
const originalEnding = detectLineEnding(content);
|
|
913
|
+
const normalizedContent = normalizeToLF(content);
|
|
914
|
+
const normalizedOldText = normalizeToLF(old_string);
|
|
915
|
+
const normalizedNewText = normalizeToLF(new_string);
|
|
916
|
+
|
|
917
|
+
// Find the old text using fuzzy matching
|
|
918
|
+
const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
|
|
919
|
+
|
|
920
|
+
if (!matchResult.found) {
|
|
921
|
+
return {
|
|
922
|
+
success: false,
|
|
923
|
+
message: `Could not find the exact text in ${file_path}. The old text must match exactly including all whitespace and newlines.`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Count occurrences using fuzzy-normalized content
|
|
928
|
+
const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
|
|
929
|
+
const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
|
|
930
|
+
const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
|
|
931
|
+
|
|
932
|
+
if (occurrences > 1) {
|
|
933
|
+
return {
|
|
934
|
+
success: false,
|
|
935
|
+
message: `Found ${occurrences} occurrences of the text in ${file_path}. The text must be unique. Please provide more context to make it unique.`,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Perform replacement
|
|
940
|
+
const baseContent = matchResult.contentForReplacement;
|
|
941
|
+
const newContent =
|
|
942
|
+
baseContent.substring(0, matchResult.index) +
|
|
943
|
+
normalizedNewText +
|
|
944
|
+
baseContent.substring(matchResult.index + matchResult.matchLength);
|
|
945
|
+
|
|
946
|
+
// Verify the replacement actually changed something
|
|
947
|
+
if (baseContent === newContent) {
|
|
948
|
+
return {
|
|
949
|
+
success: false,
|
|
950
|
+
message: `No changes made to ${file_path}. The replacement produced identical content.`,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
955
|
+
await fs.writeFile(absolutePath, finalContent, "utf-8");
|
|
956
|
+
|
|
957
|
+
const diffResult = await generateDiffString(baseContent, newContent);
|
|
958
|
+
|
|
712
959
|
return {
|
|
713
960
|
success: true,
|
|
714
|
-
message: `Successfully replaced
|
|
715
|
-
|
|
961
|
+
message: `Successfully replaced text in ${file_path}.`,
|
|
962
|
+
diff: diffResult.diff,
|
|
963
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
716
964
|
};
|
|
717
965
|
} catch (error: any) {
|
|
718
|
-
|
|
966
|
+
return {
|
|
967
|
+
success: false,
|
|
968
|
+
message: `Failed to edit file ${file_path}: ${error.message}`,
|
|
969
|
+
};
|
|
719
970
|
}
|
|
720
971
|
}
|
|
721
|
-
|
|
722
|
-
private escapeRegExp(string: string): string {
|
|
723
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
724
|
-
}
|
|
725
972
|
}
|
|
726
973
|
|
|
727
974
|
export class WebSearchTool implements Tool {
|
|
@@ -1636,23 +1883,92 @@ export class TaskTool implements Tool {
|
|
|
1636
1883
|
// Check cancellation before tool execution
|
|
1637
1884
|
checkCancellation();
|
|
1638
1885
|
|
|
1639
|
-
const toolResult = await cancellationManager.withCancellation(
|
|
1886
|
+
const toolResult: any = await cancellationManager.withCancellation(
|
|
1640
1887
|
toolRegistry.execute(name, parsedParams, mode, indent),
|
|
1641
1888
|
`subagent-${subagent_type}-${name}-${iteration}`
|
|
1642
1889
|
);
|
|
1643
1890
|
|
|
1644
|
-
//
|
|
1891
|
+
// Get showToolDetails config to control result display
|
|
1892
|
+
const showToolDetails = config.get('showToolDetails') || false;
|
|
1893
|
+
|
|
1894
|
+
// Prepare result preview for history
|
|
1645
1895
|
const resultPreview = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
|
|
1646
1896
|
const truncatedPreview = resultPreview.length > 200 ? resultPreview.substring(0, 200) + '...' : resultPreview;
|
|
1647
|
-
const indentedPreview = indentMultiline(truncatedPreview, indent);
|
|
1648
|
-
console.log(`${indent}${colors.success(`${icons.check} Completed`)}\n${indentedPreview}\n`);
|
|
1649
1897
|
|
|
1650
|
-
//
|
|
1898
|
+
// Special handling for different tools (consistent with session.ts display logic)
|
|
1899
|
+
const isTodoTool = name === 'todo_write' || name === 'todo_read';
|
|
1900
|
+
const isEditTool = name === 'Edit';
|
|
1901
|
+
const isWriteTool = name === 'Write';
|
|
1902
|
+
const isDeleteTool = name === 'DeleteFile';
|
|
1903
|
+
const hasDiff = isEditTool && toolResult?.diff;
|
|
1904
|
+
const hasFilePreview = isWriteTool && toolResult?.preview;
|
|
1905
|
+
const hasDeleteInfo = isDeleteTool && toolResult?.filePath;
|
|
1906
|
+
|
|
1907
|
+
// Import render functions for consistent display
|
|
1908
|
+
const { renderDiff, renderLines } = await import('./theme.js');
|
|
1909
|
+
|
|
1910
|
+
if (isTodoTool) {
|
|
1911
|
+
// Display todo list
|
|
1912
|
+
console.log(`${indent}${colors.success(`${icons.check} Todo List:`)}`);
|
|
1913
|
+
const todos = toolResult?.todos || [];
|
|
1914
|
+
if (todos.length === 0) {
|
|
1915
|
+
console.log(`${indent} ${colors.textMuted('No tasks')}`);
|
|
1916
|
+
} else {
|
|
1917
|
+
const statusConfig: Record<string, { icon: string; color: (text: string) => string; label: string }> = {
|
|
1918
|
+
'pending': { icon: icons.circle, color: colors.textMuted, label: 'Pending' },
|
|
1919
|
+
'in_progress': { icon: icons.loading, color: colors.warning, label: 'In Progress' },
|
|
1920
|
+
'completed': { icon: icons.success, color: colors.success, label: 'Completed' },
|
|
1921
|
+
'failed': { icon: icons.error, color: colors.error, label: 'Failed' }
|
|
1922
|
+
};
|
|
1923
|
+
for (const todo of todos) {
|
|
1924
|
+
const status = statusConfig[todo.status] || statusConfig['pending'];
|
|
1925
|
+
console.log(`${indent} ${status.color(status.icon)} ${status.color(status.label)}: ${colors.text(todo.task)}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (toolResult?.message) {
|
|
1929
|
+
console.log(`${indent}${colors.textDim(toolResult.message)}`);
|
|
1930
|
+
}
|
|
1931
|
+
console.log('');
|
|
1932
|
+
} else if (hasDiff) {
|
|
1933
|
+
// Display edit result with diff
|
|
1934
|
+
console.log('');
|
|
1935
|
+
const diffOutput = renderDiff(toolResult.diff);
|
|
1936
|
+
const indentedDiff = diffOutput.split('\n').map(line => `${indent} ${line}`).join('\n');
|
|
1937
|
+
console.log(`${indentedDiff}\n`);
|
|
1938
|
+
} else if (hasFilePreview) {
|
|
1939
|
+
// Display new file content in preview style
|
|
1940
|
+
console.log('');
|
|
1941
|
+
console.log(`${indent}${colors.success(`${icons.file} ${toolResult.filePath}`)}`);
|
|
1942
|
+
console.log(`${indent}${colors.textDim(` ${toolResult.lineCount} lines`)}`);
|
|
1943
|
+
console.log('');
|
|
1944
|
+
console.log(renderLines(toolResult.preview, { maxLines: 10, indent: indent + ' ' }));
|
|
1945
|
+
console.log('');
|
|
1946
|
+
} else if (hasDeleteInfo) {
|
|
1947
|
+
// Display DeleteFile result
|
|
1948
|
+
console.log('');
|
|
1949
|
+
console.log(`${indent}${colors.success(`${icons.check} Deleted: ${toolResult.filePath}`)}`);
|
|
1950
|
+
console.log('');
|
|
1951
|
+
} else if (showToolDetails) {
|
|
1952
|
+
// Show full result details
|
|
1953
|
+
const indentedPreview = indentMultiline(resultPreview, indent);
|
|
1954
|
+
console.log(`${indent}${colors.success(`${icons.check} Tool Result:`)}\n${indentedPreview}\n`);
|
|
1955
|
+
} else if (toolResult && toolResult.success === false) {
|
|
1956
|
+
// Tool failed
|
|
1957
|
+
console.log(`${indent}${colors.error(`${icons.cross} ${toolResult.message || 'Failed'}`)}\n`);
|
|
1958
|
+
} else if (toolResult) {
|
|
1959
|
+
// Show brief preview by default
|
|
1960
|
+
const indentedPreview = indentMultiline(truncatedPreview, indent);
|
|
1961
|
+
console.log(`${indent}${colors.success(`${icons.check} Completed`)}\n${indentedPreview}\n`);
|
|
1962
|
+
} else {
|
|
1963
|
+
console.log(`${indent}${colors.textDim('(no result)')}\n`);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Record successful tool execution in history (use truncated preview to save memory)
|
|
1651
1967
|
executionHistory.push({
|
|
1652
1968
|
tool: name,
|
|
1653
1969
|
status: 'success',
|
|
1654
1970
|
params: parsedParams,
|
|
1655
|
-
result:
|
|
1971
|
+
result: truncatedPreview,
|
|
1656
1972
|
timestamp: new Date().toISOString()
|
|
1657
1973
|
});
|
|
1658
1974
|
|
|
@@ -2469,7 +2785,7 @@ export class InvokeSkillTool implements Tool {
|
|
|
2469
2785
|
|
|
2470
2786
|
# When NOT to Use
|
|
2471
2787
|
- For simple file operations (use Read/Write instead)
|
|
2472
|
-
- For basic code changes (use
|
|
2788
|
+
- For basic code changes (use Edit/Write instead)
|
|
2473
2789
|
- When a regular tool can accomplish the task
|
|
2474
2790
|
|
|
2475
2791
|
# Parameters
|
|
@@ -2673,10 +2989,10 @@ export class ToolRegistry {
|
|
|
2673
2989
|
this.register(new GrepTool());
|
|
2674
2990
|
this.register(new BashTool());
|
|
2675
2991
|
this.register(new ListDirectoryTool());
|
|
2676
|
-
this.register(new
|
|
2992
|
+
this.register(new SearchFilesTool());
|
|
2677
2993
|
this.register(new DeleteFileTool());
|
|
2678
2994
|
this.register(new CreateDirectoryTool());
|
|
2679
|
-
this.register(new
|
|
2995
|
+
this.register(new EditTool());
|
|
2680
2996
|
this.register(new WebSearchTool());
|
|
2681
2997
|
this.register(this.todoWriteTool);
|
|
2682
2998
|
this.register(new TodoReadTool(this.todoWriteTool));
|
|
@@ -2937,7 +3253,7 @@ export class ToolRegistry {
|
|
|
2937
3253
|
};
|
|
2938
3254
|
break;
|
|
2939
3255
|
|
|
2940
|
-
case '
|
|
3256
|
+
case 'SearchFiles':
|
|
2941
3257
|
parameters = {
|
|
2942
3258
|
type: 'object',
|
|
2943
3259
|
properties: {
|
|
@@ -2948,6 +3264,10 @@ export class ToolRegistry {
|
|
|
2948
3264
|
path: {
|
|
2949
3265
|
type: 'string',
|
|
2950
3266
|
description: 'Optional: The path to search in (default: current directory)'
|
|
3267
|
+
},
|
|
3268
|
+
limit: {
|
|
3269
|
+
type: 'integer',
|
|
3270
|
+
description: 'Optional: Maximum number of results to return (default: 1000)'
|
|
2951
3271
|
}
|
|
2952
3272
|
},
|
|
2953
3273
|
required: ['pattern']
|
|
@@ -2984,13 +3304,13 @@ export class ToolRegistry {
|
|
|
2984
3304
|
};
|
|
2985
3305
|
break;
|
|
2986
3306
|
|
|
2987
|
-
case '
|
|
3307
|
+
case 'Edit':
|
|
2988
3308
|
parameters = {
|
|
2989
3309
|
type: 'object',
|
|
2990
3310
|
properties: {
|
|
2991
3311
|
file_path: {
|
|
2992
3312
|
type: 'string',
|
|
2993
|
-
description: 'The absolute path to the file'
|
|
3313
|
+
description: 'The absolute path to the file to edit'
|
|
2994
3314
|
},
|
|
2995
3315
|
instruction: {
|
|
2996
3316
|
type: 'string',
|
|
@@ -2998,11 +3318,11 @@ export class ToolRegistry {
|
|
|
2998
3318
|
},
|
|
2999
3319
|
old_string: {
|
|
3000
3320
|
type: 'string',
|
|
3001
|
-
description: 'The exact text to replace'
|
|
3321
|
+
description: 'The exact text to replace (supports fuzzy matching)'
|
|
3002
3322
|
},
|
|
3003
3323
|
new_string: {
|
|
3004
3324
|
type: 'string',
|
|
3005
|
-
description: 'The
|
|
3325
|
+
description: 'The new text to replace with'
|
|
3006
3326
|
}
|
|
3007
3327
|
},
|
|
3008
3328
|
required: ['file_path', 'instruction', 'old_string', 'new_string']
|