@gaunt-sloth/agent 2.0.0-alpha.22 → 2.0.0-alpha.24
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 +73 -24
- package/dist/core/GthDeepAgent.js +49 -2
- package/dist/core/GthDeepAgent.js.map +1 -1
- package/dist/core/debugCapture.d.ts +1 -1
- package/dist/core/debugCapture.js.map +1 -1
- package/dist/core/subagentProfiles.d.ts +50 -0
- package/dist/core/subagentProfiles.js +81 -0
- package/dist/core/subagentProfiles.js.map +1 -0
- package/dist/middleware/binaryContentInjectionMiddleware.d.ts +8 -1
- package/dist/middleware/binaryContentInjectionMiddleware.js +11 -2
- package/dist/middleware/binaryContentInjectionMiddleware.js.map +1 -1
- package/dist/middleware/frontendImageInjectionMiddleware.d.ts +80 -0
- package/dist/middleware/frontendImageInjectionMiddleware.js +146 -0
- package/dist/middleware/frontendImageInjectionMiddleware.js.map +1 -0
- package/dist/middleware/registry.js +36 -1
- package/dist/middleware/registry.js.map +1 -1
- package/dist/middleware/types.d.ts +16 -2
- package/dist/modules/a2a/A2AClientWrapper.d.ts +1 -1
- package/dist/modules/a2a/A2AClientWrapper.js +19 -5
- package/dist/modules/a2a/A2AClientWrapper.js.map +1 -1
- package/dist/modules/apiAgUiModule.d.ts +68 -0
- package/dist/modules/apiAgUiModule.js +95 -4
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.js +110 -34
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/modules/slashCommands.d.ts +328 -0
- package/dist/modules/slashCommands.js +598 -0
- package/dist/modules/slashCommands.js.map +1 -0
- package/dist/resolvers.js +15 -0
- package/dist/resolvers.js.map +1 -1
- package/dist/tools/GthCustomToolkit.js +72 -3
- package/dist/tools/GthCustomToolkit.js.map +1 -1
- package/dist/tools/GthDevToolkit.d.ts +5 -2
- package/dist/tools/GthDevToolkit.js +28 -7
- package/dist/tools/GthDevToolkit.js.map +1 -1
- package/dist/tools/GthFileSystemToolkit.d.ts +16 -0
- package/dist/tools/GthFileSystemToolkit.js +218 -110
- package/dist/tools/GthFileSystemToolkit.js.map +1 -1
- package/dist/tools/McpResourceTool.d.ts +31 -0
- package/dist/tools/McpResourceTool.js +106 -0
- package/dist/tools/McpResourceTool.js.map +1 -0
- package/dist/tools/shell/hardline.js +3 -3
- package/dist/tools/shell/hardline.js.map +1 -1
- package/package.json +2 -2
- package/dist/tools/shell/allowlist.d.ts +0 -11
- package/dist/tools/shell/allowlist.js +0 -12
- package/dist/tools/shell/allowlist.js.map +0 -1
- package/dist/tools/shell/arity.d.ts +0 -11
- package/dist/tools/shell/arity.js +0 -12
- package/dist/tools/shell/arity.js.map +0 -1
- package/dist/tools/shell/normalize.d.ts +0 -10
- package/dist/tools/shell/normalize.js +0 -11
- package/dist/tools/shell/normalize.js.map +0 -1
|
@@ -4,7 +4,6 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { createTwoFilesPatch } from 'diff';
|
|
7
|
-
import { displayInfo } from '@gaunt-sloth/core/utils/consoleUtils.js';
|
|
8
7
|
import { shouldIgnoreFile } from '@gaunt-sloth/core/utils/aiignoreUtils.js';
|
|
9
8
|
import { getCurrentWorkDir } from '@gaunt-sloth/core/utils/systemUtils.js';
|
|
10
9
|
import { getFormatForExtension, getMimeType, readBinaryFile } from '#src/tools/binaryUtils.js';
|
|
@@ -345,9 +344,13 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
345
344
|
const occurrences = this.countOccurrences(modifiedContent, normalizedOld);
|
|
346
345
|
if (occurrences > 0) {
|
|
347
346
|
if (occurrences > 1 && edit.replaceAll !== true) {
|
|
348
|
-
// Ambiguous match: refuse rather than silently editing the first occurrence.
|
|
349
|
-
|
|
350
|
-
|
|
347
|
+
// Ambiguous match: refuse rather than silently editing the first occurrence. Thrown here
|
|
348
|
+
// and caught at the tool boundary (GS2-36), which surfaces it as a recoverable errored
|
|
349
|
+
// result naming the path — so the model can add surrounding context and retry instead of
|
|
350
|
+
// the run aborting. Phrased as an action-oriented recovery hint (EXT-34 hermes style).
|
|
351
|
+
throw new Error(`found ${occurrences} occurrences of the search text, so the edit is ambiguous and the ` +
|
|
352
|
+
'file was NOT modified. Add more surrounding context to make the match unique, or set ' +
|
|
353
|
+
'replaceAll: true to replace every occurrence.\n' +
|
|
351
354
|
edit.oldText);
|
|
352
355
|
}
|
|
353
356
|
// split/join replaces every occurrence without regex escaping and, unlike
|
|
@@ -387,7 +390,14 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
387
390
|
}
|
|
388
391
|
}
|
|
389
392
|
if (!matchFound) {
|
|
390
|
-
|
|
393
|
+
// No occurrence of the search text (exact or fuzzy) — thrown here and caught at the tool
|
|
394
|
+
// boundary (GS2-36) so it reaches the model as a recoverable errored result naming the
|
|
395
|
+
// path, not a fatal abort. Action-oriented recovery hint (EXT-34 hermes style): re-read to
|
|
396
|
+
// get the exact current text, or fall back to a whole-file write_file.
|
|
397
|
+
throw new Error('no occurrence of the search text was found, so the file was NOT modified. Re-read the ' +
|
|
398
|
+
'file to get its exact current text (whitespace and indentation must match), then retry ' +
|
|
399
|
+
'the edit, or use write_file to replace the whole file.\n' +
|
|
400
|
+
edit.oldText);
|
|
391
401
|
}
|
|
392
402
|
// Fuzzy (trim-based line) match applied: flag it so the model knows the edit
|
|
393
403
|
// landed on a near-match, not an exact one.
|
|
@@ -616,12 +626,30 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
616
626
|
}
|
|
617
627
|
return result;
|
|
618
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* GS2-36 — turn a fatal tool throw into a recoverable, action-oriented errored result.
|
|
631
|
+
*
|
|
632
|
+
* A filesystem tool that throws (a denied/symlinked-out path from validatePath, a missing file,
|
|
633
|
+
* EISDIR, permission-denied, a directory-not-empty refusal, …) otherwise propagates to
|
|
634
|
+
* GthAgentRunner as a fatal `Stream processing failed` and crashes the whole turn. The house
|
|
635
|
+
* convention (AGENTS.md: a tool result must never abort a run; the write_file precedent in
|
|
636
|
+
* `5892801a`) is to return the failure to the model as a plain string it can act on. This wraps
|
|
637
|
+
* that string in a uniform `Error <action>: <message>` shape — the underlying fs/validation
|
|
638
|
+
* message (ENOENT / EISDIR / EACCES / "Access denied - …") is itself the actionable detail, and
|
|
639
|
+
* the deliberately-thrown logical messages (edit_file no-match/ambiguous, "Directory not empty",
|
|
640
|
+
* "Cannot delete protected directory") are already phrased as recovery hints. EXT-14 containment
|
|
641
|
+
* is preserved everywhere: validatePath runs first and throws on a symlink-out BEFORE any fs
|
|
642
|
+
* mutation, so the mutation never happens — the denial is merely surfaced as a result here.
|
|
643
|
+
*/
|
|
644
|
+
recoverableError(action, error) {
|
|
645
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
646
|
+
return `Error ${action}: ${message}`;
|
|
647
|
+
}
|
|
619
648
|
createReadBinaryTool() {
|
|
620
649
|
return createGthTool(async (args) => {
|
|
621
650
|
if (!this.binaryFormats || !Array.isArray(this.binaryFormats)) {
|
|
622
651
|
return 'Binary formats are not configured. Add binaryFormats to your config to enable this feature.';
|
|
623
652
|
}
|
|
624
|
-
displayInfo(`\n📁 Reading binary file: ${args.path}\n`);
|
|
625
653
|
let validPath;
|
|
626
654
|
try {
|
|
627
655
|
validPath = await this.validatePath(args.path);
|
|
@@ -678,7 +706,9 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
678
706
|
createTools() {
|
|
679
707
|
const tools = [
|
|
680
708
|
createGthTool(async (args) => {
|
|
681
|
-
|
|
709
|
+
// TUI-C32 residual b — the legacy `📁 Reading file: …` notice is dropped: the surface-
|
|
710
|
+
// agnostic tool-call block (`✓ 📁 read_file(path=…)`, TUI-C30) now announces the op once
|
|
711
|
+
// on both surfaces, so the old notice was a DL-10 double announcement.
|
|
682
712
|
const hasWindow = args.offset !== undefined || args.limit !== undefined;
|
|
683
713
|
const hasHeadTail = Boolean(args.head) || Boolean(args.tail);
|
|
684
714
|
// Argument guard first (pure, no filesystem access): the line window (offset/limit) and
|
|
@@ -690,42 +720,50 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
690
720
|
'Use offset/limit to read an arbitrary range (offset = 1-based first line, ' +
|
|
691
721
|
'limit = number of lines); use head/tail for the first/last N lines.');
|
|
692
722
|
}
|
|
693
|
-
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
?
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
723
|
+
// GS2-36: a missing file, a denied / symlinked-out path (validatePath throws before any
|
|
724
|
+
// read), or a read failure (EISDIR, EACCES) becomes a recoverable errored result the
|
|
725
|
+
// model can act on, instead of a fatal `Stream processing failed` that aborts the run.
|
|
726
|
+
try {
|
|
727
|
+
const validPath = await this.validatePath(args.path);
|
|
728
|
+
// Line window (offset/limit). offset defaults to 1; limit defaults to and is hard-capped
|
|
729
|
+
// at MAX_READ_LINES. windowFile returns from offset to EOF and capReadContent (maxLines =
|
|
730
|
+
// limit) does the slicing, so a limit-bounded read that leaves more file behind is
|
|
731
|
+
// *observable* and gets a resume notice. That notice fires only when the limit is the
|
|
732
|
+
// hard envelope — defaulted, or an explicit limit clamped down to MAX_READ_LINES — so a
|
|
733
|
+
// deliberately small explicit window (fully satisfied) stays quiet.
|
|
734
|
+
if (hasWindow) {
|
|
735
|
+
const offset = args.offset !== undefined ? Math.max(1, Math.trunc(args.offset)) : 1;
|
|
736
|
+
const limitWasDefaulted = args.limit === undefined;
|
|
737
|
+
const limitWasClamped = args.limit !== undefined && Math.trunc(args.limit) > MAX_READ_LINES;
|
|
738
|
+
const limit = limitWasDefaulted
|
|
739
|
+
? MAX_READ_LINES
|
|
740
|
+
: Math.min(Math.max(1, Math.trunc(args.limit)), MAX_READ_LINES);
|
|
741
|
+
const lineCapIsHard = limitWasDefaulted || limitWasClamped;
|
|
742
|
+
const windowed = await this.windowFile(validPath, offset);
|
|
743
|
+
return this.capReadContent(windowed, limit, offset, lineCapIsHard);
|
|
744
|
+
}
|
|
745
|
+
// head/tail paths respect the explicit line count the model asked for (maxLines =
|
|
746
|
+
// Infinity, so the line cap never fires — lineCapIsHard is moot), but still get the
|
|
747
|
+
// per-line and byte safety envelope — e.g. a `tail 1` that returns one multi-MB minified
|
|
748
|
+
// line is still truncated. No resume offset is emitted for these paths because the first
|
|
749
|
+
// emitted line is not line 1 of the file.
|
|
750
|
+
if (args.head && args.tail) {
|
|
751
|
+
return this.capReadContent(await this.headAndTailFile(validPath, args.head, args.tail), Number.POSITIVE_INFINITY, undefined, false);
|
|
752
|
+
}
|
|
753
|
+
if (args.tail) {
|
|
754
|
+
return this.capReadContent(await this.tailFile(validPath, args.tail), Number.POSITIVE_INFINITY, undefined, false);
|
|
755
|
+
}
|
|
756
|
+
if (args.head) {
|
|
757
|
+
return this.capReadContent(await this.headFile(validPath, args.head), Number.POSITIVE_INFINITY, undefined, false);
|
|
758
|
+
}
|
|
759
|
+
// Default whole-file read: capped at MAX_READ_LINES / MAX_READ_BYTES with per-line
|
|
760
|
+
// truncation (the line cap here IS the hard envelope). Files under the caps come back
|
|
761
|
+
// byte-identical to the old uncapped read.
|
|
762
|
+
return this.capReadContent(await fs.readFile(validPath, 'utf-8'), MAX_READ_LINES, 1, true);
|
|
721
763
|
}
|
|
722
|
-
|
|
723
|
-
return this.
|
|
764
|
+
catch (error) {
|
|
765
|
+
return this.recoverableError(`reading file ${args.path}`, error);
|
|
724
766
|
}
|
|
725
|
-
// Default whole-file read: capped at MAX_READ_LINES / MAX_READ_BYTES with per-line
|
|
726
|
-
// truncation (the line cap here IS the hard envelope). Files under the caps come back
|
|
727
|
-
// byte-identical to the old uncapped read.
|
|
728
|
-
return this.capReadContent(await fs.readFile(validPath, 'utf-8'), MAX_READ_LINES, 1, true);
|
|
729
767
|
}, {
|
|
730
768
|
name: 'read_file',
|
|
731
769
|
description: 'Read the complete contents of a file from the file system. ' +
|
|
@@ -746,7 +784,6 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
746
784
|
schema: ReadFileArgsSchema,
|
|
747
785
|
}, 'read'),
|
|
748
786
|
createGthTool(async (args) => {
|
|
749
|
-
displayInfo(`\n📁 Reading ${args.paths.length} files\n`);
|
|
750
787
|
const results = await Promise.all(args.paths.map(async (filePath) => {
|
|
751
788
|
try {
|
|
752
789
|
const validPath = await this.validatePath(filePath);
|
|
@@ -783,7 +820,8 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
783
820
|
schema: ReadMultipleFilesArgsSchema,
|
|
784
821
|
}, 'read'),
|
|
785
822
|
createGthTool(async (args) => {
|
|
786
|
-
|
|
823
|
+
// TUI-C32 residual b — legacy `📁 Writing file: …` notice dropped (see read_file above);
|
|
824
|
+
// the TUI-C30 tool-call block announces the write once on both surfaces.
|
|
787
825
|
try {
|
|
788
826
|
const validPath = await this.validatePath(args.path);
|
|
789
827
|
// Create any missing parent directories (mkdir -p) so a write into a
|
|
@@ -813,9 +851,20 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
813
851
|
schema: WriteFileArgsSchema,
|
|
814
852
|
}, 'write'),
|
|
815
853
|
createGthTool(async (args) => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
854
|
+
// TUI-C32 residual b — legacy `📁 Editing file: …` notice dropped (see read_file above);
|
|
855
|
+
// the TUI-C30 tool-call block announces the edit once on both surfaces.
|
|
856
|
+
// GS2-36: a no-match / ambiguous edit, a missing file, a denied or symlinked-out path
|
|
857
|
+
// (validatePath throws BEFORE applyFileEdits mutates anything — EXT-14 containment
|
|
858
|
+
// preserved), or a write failure becomes a recoverable errored result the model can act
|
|
859
|
+
// on, instead of a fatal throw that aborts the whole run. applyFileEdits' no-match /
|
|
860
|
+
// ambiguous messages are already phrased as action-oriented recovery hints.
|
|
861
|
+
try {
|
|
862
|
+
const validPath = await this.validatePath(args.path);
|
|
863
|
+
return await this.applyFileEdits(validPath, args.edits, args.dryRun);
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
return this.recoverableError(`editing file ${args.path}`, error);
|
|
867
|
+
}
|
|
819
868
|
}, {
|
|
820
869
|
name: 'edit_file',
|
|
821
870
|
description: 'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
|
|
@@ -829,10 +878,16 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
829
878
|
schema: EditFileArgsSchema,
|
|
830
879
|
}, 'write'),
|
|
831
880
|
createGthTool(async (args) => {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
881
|
+
// GS2-36: a denied / symlinked-out path or an mkdir failure (e.g. a file already exists at
|
|
882
|
+
// the path) becomes a recoverable errored result rather than a fatal run abort.
|
|
883
|
+
try {
|
|
884
|
+
const validPath = await this.validatePath(args.path);
|
|
885
|
+
await fs.mkdir(validPath, { recursive: true });
|
|
886
|
+
return `Successfully created directory ${args.path}`;
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
return this.recoverableError(`creating directory ${args.path}`, error);
|
|
890
|
+
}
|
|
836
891
|
}, {
|
|
837
892
|
name: 'create_directory',
|
|
838
893
|
description: 'Create a new directory or ensure a directory exists. Can create multiple ' +
|
|
@@ -842,17 +897,23 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
842
897
|
schema: CreateDirectoryArgsSchema,
|
|
843
898
|
}, 'write'),
|
|
844
899
|
createGthTool(async (args) => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
900
|
+
// GS2-36: a missing directory, a denied / symlinked-out path, or a not-a-directory
|
|
901
|
+
// (ENOTDIR) error becomes a recoverable errored result rather than a fatal run abort.
|
|
902
|
+
try {
|
|
903
|
+
const validPath = await this.validatePath(args.path);
|
|
904
|
+
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
905
|
+
const aiignoreConfig = this.aiignoreConfig;
|
|
906
|
+
const filteredEntries = entries.filter((entry) => {
|
|
907
|
+
const fullPath = path.join(validPath, entry.name);
|
|
908
|
+
return !shouldIgnoreFile(fullPath, getCurrentWorkDir(), aiignoreConfig?.patterns, aiignoreConfig?.enabled);
|
|
909
|
+
});
|
|
910
|
+
return filteredEntries
|
|
911
|
+
.map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
|
|
912
|
+
.join('\n');
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
return this.recoverableError(`listing directory ${args.path}`, error);
|
|
916
|
+
}
|
|
856
917
|
}, {
|
|
857
918
|
name: 'list_directory',
|
|
858
919
|
description: 'Get a detailed listing of all files and directories in a specified path. ' +
|
|
@@ -862,9 +923,18 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
862
923
|
schema: ListDirectoryArgsSchema,
|
|
863
924
|
}, 'read'),
|
|
864
925
|
createGthTool(async (args) => {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
926
|
+
// GS2-36: a missing directory or denied path becomes a recoverable errored result rather
|
|
927
|
+
// than a fatal run abort. Only validatePath + readdir can throw fatally here (the per-entry
|
|
928
|
+
// stat below already has its own try/catch), so guard just those.
|
|
929
|
+
let validPath;
|
|
930
|
+
let entries;
|
|
931
|
+
try {
|
|
932
|
+
validPath = await this.validatePath(args.path);
|
|
933
|
+
entries = (await fs.readdir(validPath, { withFileTypes: true }));
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
return this.recoverableError(`listing directory ${args.path}`, error);
|
|
937
|
+
}
|
|
868
938
|
const aiignoreConfig = this.aiignoreConfig;
|
|
869
939
|
const filteredEntries = entries.filter((entry) => {
|
|
870
940
|
const fullPath = path.join(validPath, entry.name);
|
|
@@ -915,7 +985,6 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
915
985
|
schema: ListDirectoryWithSizesArgsSchema,
|
|
916
986
|
}, 'read'),
|
|
917
987
|
createGthTool(async (args) => {
|
|
918
|
-
displayInfo(`\n📁 Building directory tree: ${args.path}\n`);
|
|
919
988
|
const buildTree = async (currentPath) => {
|
|
920
989
|
const validPath = await this.validatePath(currentPath);
|
|
921
990
|
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
@@ -943,8 +1012,16 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
943
1012
|
}
|
|
944
1013
|
return result;
|
|
945
1014
|
};
|
|
946
|
-
|
|
947
|
-
|
|
1015
|
+
// GS2-36: a missing directory, denied path, or a symlink-out encountered mid-walk
|
|
1016
|
+
// (buildTree calls validatePath on each level) becomes a recoverable errored result
|
|
1017
|
+
// rather than a fatal run abort.
|
|
1018
|
+
try {
|
|
1019
|
+
const treeData = await buildTree(args.path);
|
|
1020
|
+
return JSON.stringify(treeData, null, 2);
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
return this.recoverableError(`building directory tree for ${args.path}`, error);
|
|
1024
|
+
}
|
|
948
1025
|
}, {
|
|
949
1026
|
name: 'directory_tree',
|
|
950
1027
|
description: 'Get a recursive tree view of files and directories as a JSON structure. ' +
|
|
@@ -954,11 +1031,17 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
954
1031
|
schema: DirectoryTreeArgsSchema,
|
|
955
1032
|
}, 'read'),
|
|
956
1033
|
createGthTool(async (args) => {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1034
|
+
// GS2-36: a missing source, an existing destination, or a denied / symlinked-out path
|
|
1035
|
+
// becomes a recoverable errored result rather than a fatal run abort.
|
|
1036
|
+
try {
|
|
1037
|
+
const validSourcePath = await this.validatePath(args.source);
|
|
1038
|
+
const validDestPath = await this.validatePath(args.destination);
|
|
1039
|
+
await fs.rename(validSourcePath, validDestPath);
|
|
1040
|
+
return `Successfully moved ${args.source} to ${args.destination}`;
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
return this.recoverableError(`moving ${args.source} to ${args.destination}`, error);
|
|
1044
|
+
}
|
|
962
1045
|
}, {
|
|
963
1046
|
name: 'move_file',
|
|
964
1047
|
description: 'Move or rename files and directories. Can move files between directories ' +
|
|
@@ -968,10 +1051,16 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
968
1051
|
schema: MoveFileArgsSchema,
|
|
969
1052
|
}, 'write'),
|
|
970
1053
|
createGthTool(async (args) => {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1054
|
+
// GS2-36: a missing or denied root path becomes a recoverable errored result rather than a
|
|
1055
|
+
// fatal run abort (searchFiles already swallows per-entry errors internally).
|
|
1056
|
+
try {
|
|
1057
|
+
const validPath = await this.validatePath(args.path);
|
|
1058
|
+
const results = await this.searchFiles(validPath, args.pattern, args.excludePatterns);
|
|
1059
|
+
return results.length > 0 ? results.join('\n') : 'No matches found';
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
return this.recoverableError(`searching ${args.path}`, error);
|
|
1063
|
+
}
|
|
975
1064
|
}, {
|
|
976
1065
|
name: 'search_files',
|
|
977
1066
|
description: 'Recursively search for files and directories matching a pattern. ' +
|
|
@@ -982,12 +1071,18 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
982
1071
|
schema: SearchFilesArgsSchema,
|
|
983
1072
|
}, 'read'),
|
|
984
1073
|
createGthTool(async (args) => {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
.
|
|
1074
|
+
// GS2-36: a missing file or denied path becomes a recoverable errored result rather than a
|
|
1075
|
+
// fatal run abort.
|
|
1076
|
+
try {
|
|
1077
|
+
const validPath = await this.validatePath(args.path);
|
|
1078
|
+
const info = await this.getFileStats(validPath);
|
|
1079
|
+
return Object.entries(info)
|
|
1080
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
1081
|
+
.join('\n');
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
return this.recoverableError(`getting file info for ${args.path}`, error);
|
|
1085
|
+
}
|
|
991
1086
|
}, {
|
|
992
1087
|
name: 'get_file_info',
|
|
993
1088
|
description: 'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
|
|
@@ -997,14 +1092,20 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
997
1092
|
schema: GetFileInfoArgsSchema,
|
|
998
1093
|
}, 'read'),
|
|
999
1094
|
createGthTool(async (args) => {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1095
|
+
// GS2-36: a missing file, a denied path, or the "this is a directory" refusal becomes a
|
|
1096
|
+
// recoverable errored result the model can act on, rather than a fatal run abort.
|
|
1097
|
+
try {
|
|
1098
|
+
const validPath = await this.validatePath(args.path);
|
|
1099
|
+
const stats = await fs.stat(validPath);
|
|
1100
|
+
if (stats.isDirectory()) {
|
|
1101
|
+
throw new Error(`Cannot delete directory: ${args.path}. Use rmdir or a recursive delete tool for directories.`);
|
|
1102
|
+
}
|
|
1103
|
+
await fs.unlink(validPath);
|
|
1104
|
+
return `Successfully deleted file: ${args.path}`;
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
return this.recoverableError(`deleting file ${args.path}`, error);
|
|
1005
1108
|
}
|
|
1006
|
-
await fs.unlink(validPath);
|
|
1007
|
-
return `Successfully deleted file: ${args.path}`;
|
|
1008
1109
|
}, {
|
|
1009
1110
|
name: 'delete_file',
|
|
1010
1111
|
description: 'Delete a file from the filesystem. This operation cannot be undone. ' +
|
|
@@ -1013,28 +1114,35 @@ export default class GthFileSystemToolkit extends BaseToolkit {
|
|
|
1013
1114
|
schema: DeleteFileArgsSchema,
|
|
1014
1115
|
}, 'write'),
|
|
1015
1116
|
createGthTool(async (args) => {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const entries = await fs.readdir(validPath);
|
|
1033
|
-
if (entries.length > 0) {
|
|
1034
|
-
throw new Error(`Directory not empty: ${args.path}. Use recursive: true to delete non-empty directories.`);
|
|
1117
|
+
// GS2-36: a missing directory, a denied path, the protected-root / not-a-directory /
|
|
1118
|
+
// not-empty refusals all become recoverable errored results the model can act on, rather
|
|
1119
|
+
// than a fatal run abort. Each refusal message is already an action-oriented hint.
|
|
1120
|
+
try {
|
|
1121
|
+
const validPath = await this.validatePath(args.path);
|
|
1122
|
+
// Check if this is a protected directory
|
|
1123
|
+
if (this.isProtectedDirectory(validPath)) {
|
|
1124
|
+
throw new Error(`Cannot delete protected directory: ${args.path}. This is one of the allowed root directories.`);
|
|
1125
|
+
}
|
|
1126
|
+
const stats = await fs.stat(validPath);
|
|
1127
|
+
if (!stats.isDirectory()) {
|
|
1128
|
+
throw new Error(`Not a directory: ${args.path}. Use delete_file for files.`);
|
|
1129
|
+
}
|
|
1130
|
+
if (args.recursive) {
|
|
1131
|
+
await fs.rm(validPath, { recursive: true, force: true });
|
|
1132
|
+
return `Successfully deleted directory and all contents: ${args.path}`;
|
|
1035
1133
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1134
|
+
else {
|
|
1135
|
+
// For non-recursive delete, check if directory is empty
|
|
1136
|
+
const entries = await fs.readdir(validPath);
|
|
1137
|
+
if (entries.length > 0) {
|
|
1138
|
+
throw new Error(`Directory not empty: ${args.path}. Use recursive: true to delete non-empty directories.`);
|
|
1139
|
+
}
|
|
1140
|
+
await fs.rmdir(validPath);
|
|
1141
|
+
return `Successfully deleted empty directory: ${args.path}`;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
return this.recoverableError(`deleting directory ${args.path}`, error);
|
|
1038
1146
|
}
|
|
1039
1147
|
}, {
|
|
1040
1148
|
name: 'delete_directory',
|