Skip to content

Commit 93e227f

Browse files
authored
Merge pull request #46 from AgentSwarm-AI/AGT-26-CLI-list_directory_files
Agt 26 cli list directory files
2 parents d5c7bd5 + 871365c commit 93e227f

7 files changed

Lines changed: 370 additions & 2 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { DirectoryScanner } from "@/services/FileManagement/DirectoryScanner";
2+
import { DebugLogger } from "@/services/logging/DebugLogger";
3+
import fs from "fs";
4+
import path from "path";
5+
import { autoInjectable } from "tsyringe";
6+
import { ActionTagsExtractor } from "./ActionTagsExtractor";
7+
import { BaseAction } from "./core/BaseAction";
8+
import { IActionBlueprint } from "./core/IAction";
9+
import { IActionResult } from "./types/ActionTypes";
10+
11+
interface IListDirectoryFilesParams {
12+
path: string | string[];
13+
recursive?: boolean;
14+
}
15+
16+
@autoInjectable()
17+
export class ListDirectoryFilesAction extends BaseAction {
18+
protected getBlueprint(): IActionBlueprint {
19+
return {
20+
tag: "list_directory_files",
21+
class: ListDirectoryFilesAction,
22+
description: "Lists files in one or more directories",
23+
usageExplanation:
24+
"Use this action to list all files within one or more directories. Optionally, enable recursive listing.",
25+
parameters: [
26+
{
27+
name: "path",
28+
required: true,
29+
description:
30+
"Path or paths to the directories (can use multiple <path> tags)",
31+
mayContainNestedContent: false,
32+
},
33+
{
34+
name: "recursive",
35+
required: false,
36+
description: "Enable recursive listing",
37+
mayContainNestedContent: false,
38+
},
39+
],
40+
};
41+
}
42+
43+
constructor(
44+
protected actionTagsExtractor: ActionTagsExtractor,
45+
private directoryScanner: DirectoryScanner,
46+
private debugLogger: DebugLogger,
47+
) {
48+
super(actionTagsExtractor);
49+
}
50+
51+
protected validateParams(params: IListDirectoryFilesParams): string | null {
52+
if (!params.path) {
53+
return "Must include at least one <path> tag";
54+
}
55+
56+
const paths = Array.isArray(params.path) ? params.path : [params.path];
57+
58+
for (const dirPath of paths) {
59+
try {
60+
const absolutePath = path.resolve(dirPath);
61+
const stats = fs.statSync(absolutePath);
62+
63+
if (!stats.isDirectory()) {
64+
return `Path '${dirPath}' exists but is not a directory`;
65+
}
66+
} catch (error) {
67+
return `Invalid or inaccessible path: ${dirPath}`;
68+
}
69+
}
70+
71+
return null;
72+
}
73+
74+
protected async executeInternal(
75+
params: IListDirectoryFilesParams,
76+
): Promise<IActionResult> {
77+
const paths = Array.isArray(params.path) ? params.path : [params.path];
78+
const allResults: string[] = [];
79+
80+
for (const dirPath of paths) {
81+
const result = await this.directoryScanner.scan(dirPath, {
82+
maxDepth: params.recursive ? undefined : 1,
83+
});
84+
85+
if (!result.success || !result.data) {
86+
return this.createErrorResult(
87+
`Failed to list directory contents for path: ${dirPath}. ${result.error || ""}`,
88+
);
89+
}
90+
91+
let content: string;
92+
try {
93+
content =
94+
typeof result.data === "string"
95+
? result.data
96+
: JSON.stringify(result.data, null, 2);
97+
} catch (error) {
98+
return this.createErrorResult(
99+
`Failed to process directory contents for path: ${dirPath}. ${error.message}`,
100+
);
101+
}
102+
103+
allResults.push(content);
104+
}
105+
106+
this.logSuccess("Directory listing completed successfully.\n");
107+
return this.createSuccessResult(allResults.join("\n"));
108+
}
109+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { UnitTestMocker } from "@/jest/mocks/UnitTestMocker";
2+
import { ConfigService } from "@/services/ConfigService";
3+
import { DirectoryScanner } from "@/services/FileManagement/DirectoryScanner";
4+
import { DebugLogger } from "@/services/logging/DebugLogger";
5+
import fs from "fs";
6+
import path from "path";
7+
import { ActionTagsExtractor } from "../ActionTagsExtractor";
8+
import { ListDirectoryFilesAction } from "../ListDirectoryFilesAction";
9+
10+
describe("ListDirectoryFilesAction", () => {
11+
let action: ListDirectoryFilesAction;
12+
let mockDirectoryScanner: DirectoryScanner;
13+
let mocker: UnitTestMocker;
14+
let mockConfigService: ConfigService;
15+
let mockActionTagsExtractor: ActionTagsExtractor;
16+
let consoleErrorSpy: jest.SpyInstance;
17+
let consoleLogSpy: jest.SpyInstance;
18+
19+
beforeEach(() => {
20+
mocker = new UnitTestMocker();
21+
22+
// Mock console methods
23+
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
24+
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
25+
26+
// Mock ConfigService
27+
mockConfigService = new ConfigService();
28+
mocker.mockPrototype(ConfigService, "getConfig", {
29+
directoryScanner: {
30+
defaultIgnore: [],
31+
allFiles: true,
32+
maxDepth: 4,
33+
directoryFirst: true,
34+
excludeDirectories: [],
35+
},
36+
});
37+
38+
mockDirectoryScanner = new DirectoryScanner(mockConfigService);
39+
40+
// Mock filesystem operations
41+
mocker.mockModule(fs, "statSync", { isDirectory: () => true });
42+
mocker.mockModule(path, "resolve", (p: string) => p);
43+
44+
// Mock DirectoryScanner scan method
45+
mocker.mockPrototype(DirectoryScanner, "scan", {
46+
success: true,
47+
data: "file1.txt\nfile2.txt",
48+
});
49+
50+
// Mock ActionTagsExtractor
51+
mockActionTagsExtractor = new ActionTagsExtractor();
52+
mocker.mockPrototypeWith(
53+
ActionTagsExtractor,
54+
"extractTag",
55+
(content: string, tag: string) => {
56+
if (tag === "path") {
57+
if (content.includes("<path>./src1</path><path>./src2</path>")) {
58+
return ["./src1", "./src2"];
59+
}
60+
return "./src";
61+
}
62+
if (tag === "recursive") {
63+
return content.includes("<recursive>true</recursive>")
64+
? "true"
65+
: null;
66+
}
67+
return null;
68+
},
69+
);
70+
71+
action = new ListDirectoryFilesAction(
72+
mockActionTagsExtractor,
73+
mockDirectoryScanner,
74+
new DebugLogger(),
75+
);
76+
});
77+
78+
afterEach(() => {
79+
mocker.clearAllMocks();
80+
consoleErrorSpy.mockRestore();
81+
consoleLogSpy.mockRestore();
82+
});
83+
84+
it("should list directory contents successfully", async () => {
85+
const result = await action.execute(
86+
"<list_directory_files><path>./src</path></list_directory_files>",
87+
);
88+
89+
expect(result.success).toBe(true);
90+
expect(result.data).toBe("file1.txt\nfile2.txt");
91+
});
92+
93+
it("should handle recursive listing", async () => {
94+
mocker.mockPrototype(DirectoryScanner, "scan", {
95+
success: true,
96+
data: "dir1/file1.txt\ndir2/file2.txt",
97+
});
98+
99+
const result = await action.execute(
100+
"<list_directory_files><path>./src</path><recursive>true</recursive></list_directory_files>",
101+
);
102+
103+
expect(result.success).toBe(true);
104+
expect(result.data).toBe("dir1/file1.txt\ndir2/file2.txt");
105+
});
106+
107+
it("should handle multiple paths", async () => {
108+
const scanSpy = mocker.mockPrototypeWith(
109+
DirectoryScanner,
110+
"scan",
111+
async (path: string) => {
112+
if (path === "./src1") {
113+
return { success: true, data: "src1/file1.txt\nsrc1/file2.txt" };
114+
}
115+
return { success: true, data: "src2/file3.txt\nsrc2/file4.txt" };
116+
},
117+
);
118+
119+
const result = await action.execute(
120+
"<list_directory_files><path>./src1</path><path>./src2</path></list_directory_files>",
121+
);
122+
123+
expect(result.success).toBe(true);
124+
expect(result.data).toContain("src1/file1.txt");
125+
expect(result.data).toContain("src1/file2.txt");
126+
expect(result.data).toContain("src2/file3.txt");
127+
expect(result.data).toContain("src2/file4.txt");
128+
expect(scanSpy).toHaveBeenCalledTimes(2);
129+
expect(scanSpy).toHaveBeenCalledWith("./src1", expect.any(Object));
130+
expect(scanSpy).toHaveBeenCalledWith("./src2", expect.any(Object));
131+
});
132+
133+
it("should handle multiple paths with recursive option", async () => {
134+
const scanSpy = mocker.mockPrototypeWith(
135+
DirectoryScanner,
136+
"scan",
137+
async (path: string) => {
138+
if (path === "./src1") {
139+
return { success: true, data: "src1/deep/file1.txt\nsrc1/file2.txt" };
140+
}
141+
return { success: true, data: "src2/deep/file3.txt\nsrc2/file4.txt" };
142+
},
143+
);
144+
145+
const result = await action.execute(
146+
"<list_directory_files><path>./src1</path><path>./src2</path><recursive>true</recursive></list_directory_files>",
147+
);
148+
149+
expect(result.success).toBe(true);
150+
expect(result.data).toContain("src1/deep/file1.txt");
151+
expect(result.data).toContain("src1/file2.txt");
152+
expect(result.data).toContain("src2/deep/file3.txt");
153+
expect(result.data).toContain("src2/file4.txt");
154+
expect(scanSpy).toHaveBeenCalledTimes(2);
155+
expect(scanSpy).toHaveBeenCalledWith(
156+
"./src1",
157+
expect.objectContaining({ maxDepth: undefined }),
158+
);
159+
expect(scanSpy).toHaveBeenCalledWith(
160+
"./src2",
161+
expect.objectContaining({ maxDepth: undefined }),
162+
);
163+
});
164+
});

src/services/LLM/actions/blueprints/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { endTaskActionBlueprint } from "./endTaskActionBlueprint";
1010
import { fetchUrlActionBlueprint } from "./fetchUrlActionBlueprint";
1111
import { gitDiffActionBlueprint } from "./gitDiffActionBlueprint";
1212
import { gitPRDiffActionBlueprint } from "./gitPRDiffActionBlueprint";
13+
import { listDirectoryFilesActionBlueprint } from "./listDirectoryFilesActionBlueprint";
1314
import { moveFileActionBlueprint } from "./moveFileActionBlueprint";
14-
import { readFileActionBlueprint } from "./readFileActionBlueprint";
1515
import { readDirectoryActionBlueprint } from "./readDirectoryActionBlueprint";
16+
import { readFileActionBlueprint } from "./readFileActionBlueprint";
1617
import { relativePathLookupActionBlueprint } from "./relativePathLookupActionBlueprint";
1718
import {
1819
searchFileActionBlueprint,
@@ -37,6 +38,7 @@ export const actionsBlueprints = {
3738
[searchFileActionBlueprint.tag]: searchFileActionBlueprint,
3839
[searchStringActionBlueprint.tag]: searchStringActionBlueprint,
3940
[writeFileActionBlueprint.tag]: writeFileActionBlueprint,
41+
[listDirectoryFilesActionBlueprint.tag]: listDirectoryFilesActionBlueprint,
4042
[readDirectoryActionBlueprint.tag]: readDirectoryActionBlueprint,
4143
} as const;
4244

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { IActionBlueprint } from "../core/IAction";
2+
import { ListDirectoryFilesAction } from "../ListDirectoryFilesAction";
3+
import { ActionPriority } from "../types/ActionPriority";
4+
5+
export const listDirectoryFilesActionBlueprint: IActionBlueprint = {
6+
tag: "list_directory_files",
7+
class: ListDirectoryFilesAction,
8+
description: "Lists all file paths inside a directory",
9+
priority: ActionPriority.HIGH,
10+
canRunInParallel: true,
11+
requiresProcessing: false,
12+
usageExplanation:
13+
"<list_directory_files><path>./src</path></list_directory_files>",
14+
parameters: [
15+
{
16+
name: "path",
17+
required: true,
18+
description: "The directory path to list files from",
19+
validator: (value: unknown): value is string =>
20+
typeof value === "string" && value.length > 0,
21+
},
22+
{
23+
name: "recursive",
24+
required: false,
25+
description: "Whether to list files recursively (default: false)",
26+
validator: (value: unknown): value is boolean =>
27+
typeof value === "boolean",
28+
},
29+
],
30+
};

src/services/LLM/phases/blueprints/discoveryPhaseBlueprint.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,49 @@ Only one action per reply. Use tags properly:
3939
<path>file.ts</path>
4040
</read_file>
4141
42-
<execute_command>...</execute_command>
42+
<!-- Run typechecks and tests if needed. -->
43+
44+
<!-- Move to the next phase after completion. Do not do it in the same prompt! -->
45+
46+
Ok, I have enough context to move to the next phase.
47+
48+
<end_phase>
49+
strategy_phase
50+
</end_phase>
51+
52+
</phase_prompt>
53+
54+
## Allowed Actions
55+
<!-- Follow correct tag structure and use only one action per reply. No comments or additional text. -->
56+
57+
REMEMBER: ONLY ONE ACTION PER REPLY!!!
58+
59+
<read_file>
60+
<!-- Read individual files only, not directories -->
61+
<path>path/here</path>
62+
<!-- Do not read the same file multiple times unless changed -->
63+
<!-- Ensure correct <read_file> tag format -->
64+
<!-- Read up to 4 related files -->
65+
<!-- Multiple <path> tags allowed -->
66+
<!-- Use relative paths -->
67+
</read_file>
68+
69+
<list_directory_files>
70+
<!-- One or more paths -->
71+
<path>path/here</path>
72+
<path>path/here/2</path>
73+
<recursive>false</recursive>
74+
<!-- Use this action to LIST all files in a directory. Set recursive to true if you want to list files recursively. -->
75+
</list_directory_files>
76+
77+
78+
<execute_command>
79+
<!-- Use this if you want to explore the codebase further. Examples below: -->
80+
<!-- List files and directories: ls -->
81+
<!-- Detailed directory list: ls -lh -->
82+
<!-- Show current directory path: pwd -->
83+
</execute_command>
84+
4385
<search_string>
4486
<directory>...</directory>
4587
<term>...</term>
@@ -51,6 +93,7 @@ Only one action per reply. Use tags properly:
5193
</search_file>
5294
5395
<read_directory>
96+
<!-- This will read all files in a directory. -->
5497
<!-- One or more paths -->
5598
<path>directory/path</path>
5699
<path>directory/path/2</path>

0 commit comments

Comments
 (0)