@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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/README_CN.md +1 -1
  3. package/dist/agents.js +164 -164
  4. package/dist/agents.js.map +1 -1
  5. package/dist/ai-client.d.ts +4 -6
  6. package/dist/ai-client.d.ts.map +1 -1
  7. package/dist/ai-client.js +137 -115
  8. package/dist/ai-client.js.map +1 -1
  9. package/dist/auth.js +4 -4
  10. package/dist/auth.js.map +1 -1
  11. package/dist/cli.js +184 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.js +3 -3
  14. package/dist/config.js.map +1 -1
  15. package/dist/context-compressor.d.ts.map +1 -1
  16. package/dist/context-compressor.js +65 -81
  17. package/dist/context-compressor.js.map +1 -1
  18. package/dist/conversation.d.ts +1 -1
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +5 -31
  21. package/dist/conversation.js.map +1 -1
  22. package/dist/memory.d.ts +5 -1
  23. package/dist/memory.d.ts.map +1 -1
  24. package/dist/memory.js +77 -37
  25. package/dist/memory.js.map +1 -1
  26. package/dist/remote-ai-client.d.ts +1 -8
  27. package/dist/remote-ai-client.d.ts.map +1 -1
  28. package/dist/remote-ai-client.js +55 -65
  29. package/dist/remote-ai-client.js.map +1 -1
  30. package/dist/retry.d.ts +35 -0
  31. package/dist/retry.d.ts.map +1 -0
  32. package/dist/retry.js +166 -0
  33. package/dist/retry.js.map +1 -0
  34. package/dist/session.d.ts +0 -5
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +243 -312
  37. package/dist/session.js.map +1 -1
  38. package/dist/slash-commands.d.ts +1 -0
  39. package/dist/slash-commands.d.ts.map +1 -1
  40. package/dist/slash-commands.js +91 -9
  41. package/dist/slash-commands.js.map +1 -1
  42. package/dist/smart-approval.d.ts.map +1 -1
  43. package/dist/smart-approval.js +18 -17
  44. package/dist/smart-approval.js.map +1 -1
  45. package/dist/system-prompt-generator.d.ts.map +1 -1
  46. package/dist/system-prompt-generator.js +149 -139
  47. package/dist/system-prompt-generator.js.map +1 -1
  48. package/dist/theme.d.ts +48 -0
  49. package/dist/theme.d.ts.map +1 -1
  50. package/dist/theme.js +254 -0
  51. package/dist/theme.js.map +1 -1
  52. package/dist/tools/edit-diff.d.ts +32 -0
  53. package/dist/tools/edit-diff.d.ts.map +1 -0
  54. package/dist/tools/edit-diff.js +185 -0
  55. package/dist/tools/edit-diff.js.map +1 -0
  56. package/dist/tools/edit.d.ts +11 -0
  57. package/dist/tools/edit.d.ts.map +1 -0
  58. package/dist/tools/edit.js +129 -0
  59. package/dist/tools/edit.js.map +1 -0
  60. package/dist/tools.d.ts +19 -5
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +979 -631
  63. package/dist/tools.js.map +1 -1
  64. package/dist/types.d.ts +6 -31
  65. package/dist/types.d.ts.map +1 -1
  66. package/package.json +3 -2
  67. package/src/agents.ts +504 -504
  68. package/src/ai-client.ts +1559 -1458
  69. package/src/auth.ts +4 -4
  70. package/src/cli.ts +195 -1
  71. package/src/config.ts +3 -3
  72. package/src/memory.ts +55 -14
  73. package/src/remote-ai-client.ts +663 -683
  74. package/src/retry.ts +217 -0
  75. package/src/session.ts +1736 -1840
  76. package/src/slash-commands.ts +98 -9
  77. package/src/smart-approval.ts +626 -625
  78. package/src/system-prompt-generator.ts +853 -843
  79. package/src/theme.ts +284 -0
  80. 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 Replace instead)
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 Replace tool instead`;
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 SearchCodebase instead)
178
- - When searching by file pattern rather than content (use SearchCodebase)
179
- - For very large codebases where you only need file names (SearchCodebase is faster)
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/Replace/CreateDirectory instead)
326
+ - For file operations (use Read/Write/Edit/CreateDirectory instead)
320
327
  - For searching file content (use Grep instead)
321
- - For finding files (use SearchCodebase or ListDirectory instead)
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 SearchCodebase instead)
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 class SearchCodebaseTool implements Tool {
505
- name = 'SearchCodebase';
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: SearchCodebase(pattern="**/*.ts")
525
- - Find test files: SearchCodebase(pattern="**/*.test.ts")
526
- - Find config files: SearchCodebase(pattern="**/config.*")
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<string[]> {
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
- return files;
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
- export class ReplaceTool implements Tool {
647
- name = 'replace';
648
- description = `Replace specific text within an existing file. This is your PRIMARY tool for making targeted edits to code.
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 Replace individually)
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 of context before and after the target text
670
- - Ensure unique matching to avoid unintended replacements
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
- replace(
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
- - Be careful with special regex characters in old_string (they're escaped automatically)
684
- - If multiple occurrences exist, all will be replaced`;
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; changes: number }> {
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
- if (occurrences === 0) {
895
+ // Check if file exists
896
+ try {
897
+ await fs.access(absolutePath);
898
+ } catch {
702
899
  return {
703
900
  success: false,
704
- message: `No occurrences found to replace in ${file_path}`,
705
- changes: 0
901
+ message: `File not found: ${file_path}`,
706
902
  };
707
903
  }
708
-
709
- const newContent = content.replace(new RegExp(this.escapeRegExp(old_string), 'g'), new_string);
710
- await fs.writeFile(absolutePath, newContent, 'utf-8');
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 ${occurrences} occurrence(s) in ${file_path}`,
715
- changes: occurrences
961
+ message: `Successfully replaced text in ${file_path}.`,
962
+ diff: diffResult.diff,
963
+ firstChangedLine: diffResult.firstChangedLine,
716
964
  };
717
965
  } catch (error: any) {
718
- throw new Error(`Failed to replace in file ${file_path}: ${error.message}`);
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
- // Display tool result with proper indentation for multi-line content
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
- // Record successful tool execution in history
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: toolResult,
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 Replace/Write instead)
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 SearchCodebaseTool());
2992
+ this.register(new SearchFilesTool());
2677
2993
  this.register(new DeleteFileTool());
2678
2994
  this.register(new CreateDirectoryTool());
2679
- this.register(new ReplaceTool());
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 'SearchCodebase':
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 'replace':
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 exact text to replace with'
3325
+ description: 'The new text to replace with'
3006
3326
  }
3007
3327
  },
3008
3328
  required: ['file_path', 'instruction', 'old_string', 'new_string']