Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/services/LLM/actions/ListDirectoryFilesAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { DirectoryScanner } from "@/services/FileManagement/DirectoryScanner";
import { DebugLogger } from "@/services/logging/DebugLogger";
import fs from "fs";
import path from "path";
import { autoInjectable } from "tsyringe";
import { ActionTagsExtractor } from "./ActionTagsExtractor";
import { BaseAction } from "./core/BaseAction";
import { IActionBlueprint } from "./core/IAction";
import { IActionResult } from "./types/ActionTypes";

interface IListDirectoryFilesParams {
path: string | string[];
recursive?: boolean;
}

@autoInjectable()
export class ListDirectoryFilesAction extends BaseAction {
protected getBlueprint(): IActionBlueprint {
return {
tag: "list_directory_files",
class: ListDirectoryFilesAction,
description: "Lists files in one or more directories",
usageExplanation:
"Use this action to list all files within one or more directories. Optionally, enable recursive listing.",
parameters: [
{
name: "path",
required: true,
description:
"Path or paths to the directories (can use multiple <path> tags)",
mayContainNestedContent: false,
},
{
name: "recursive",
required: false,
description: "Enable recursive listing",
mayContainNestedContent: false,
},
],
};
}

constructor(
protected actionTagsExtractor: ActionTagsExtractor,
private directoryScanner: DirectoryScanner,
private debugLogger: DebugLogger,
) {
super(actionTagsExtractor);
}

protected validateParams(params: IListDirectoryFilesParams): string | null {
if (!params.path) {
return "Must include at least one <path> tag";
}

const paths = Array.isArray(params.path) ? params.path : [params.path];

for (const dirPath of paths) {
try {
const absolutePath = path.resolve(dirPath);
const stats = fs.statSync(absolutePath);

if (!stats.isDirectory()) {
return `Path '${dirPath}' exists but is not a directory`;
}
} catch (error) {
return `Invalid or inaccessible path: ${dirPath}`;
}
}

return null;
}

protected async executeInternal(
params: IListDirectoryFilesParams,
): Promise<IActionResult> {
const paths = Array.isArray(params.path) ? params.path : [params.path];
const allResults: string[] = [];

for (const dirPath of paths) {
const result = await this.directoryScanner.scan(dirPath, {
maxDepth: params.recursive ? undefined : 1,
});

if (!result.success || !result.data) {
return this.createErrorResult(
`Failed to list directory contents for path: ${dirPath}. ${result.error || ""}`,
);
}

let content: string;
try {
content =
typeof result.data === "string"
? result.data
: JSON.stringify(result.data, null, 2);
} catch (error) {
return this.createErrorResult(
`Failed to process directory contents for path: ${dirPath}. ${error.message}`,
);
}

allResults.push(content);
}

this.logSuccess("Directory listing completed successfully.\n");
return this.createSuccessResult(allResults.join("\n"));
}
}
164 changes: 164 additions & 0 deletions src/services/LLM/actions/__tests__/ListDirectoryFilesAction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { UnitTestMocker } from "@/jest/mocks/UnitTestMocker";
import { ConfigService } from "@/services/ConfigService";
import { DirectoryScanner } from "@/services/FileManagement/DirectoryScanner";
import { DebugLogger } from "@/services/logging/DebugLogger";
import fs from "fs";
import path from "path";
import { ActionTagsExtractor } from "../ActionTagsExtractor";
import { ListDirectoryFilesAction } from "../ListDirectoryFilesAction";

describe("ListDirectoryFilesAction", () => {
let action: ListDirectoryFilesAction;
let mockDirectoryScanner: DirectoryScanner;
let mocker: UnitTestMocker;
let mockConfigService: ConfigService;
let mockActionTagsExtractor: ActionTagsExtractor;
let consoleErrorSpy: jest.SpyInstance;
let consoleLogSpy: jest.SpyInstance;

beforeEach(() => {
mocker = new UnitTestMocker();

// Mock console methods
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});

// Mock ConfigService
mockConfigService = new ConfigService();
mocker.mockPrototype(ConfigService, "getConfig", {
directoryScanner: {
defaultIgnore: [],
allFiles: true,
maxDepth: 4,
directoryFirst: true,
excludeDirectories: [],
},
});

mockDirectoryScanner = new DirectoryScanner(mockConfigService);

// Mock filesystem operations
mocker.mockModule(fs, "statSync", { isDirectory: () => true });
mocker.mockModule(path, "resolve", (p: string) => p);

// Mock DirectoryScanner scan method
mocker.mockPrototype(DirectoryScanner, "scan", {
success: true,
data: "file1.txt\nfile2.txt",
});

// Mock ActionTagsExtractor
mockActionTagsExtractor = new ActionTagsExtractor();
mocker.mockPrototypeWith(
ActionTagsExtractor,
"extractTag",
(content: string, tag: string) => {
if (tag === "path") {
if (content.includes("<path>./src1</path><path>./src2</path>")) {
return ["./src1", "./src2"];
}
return "./src";
}
if (tag === "recursive") {
return content.includes("<recursive>true</recursive>")
? "true"
: null;
}
return null;
},
);

action = new ListDirectoryFilesAction(
mockActionTagsExtractor,
mockDirectoryScanner,
new DebugLogger(),
);
});

afterEach(() => {
mocker.clearAllMocks();
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
});

it("should list directory contents successfully", async () => {
const result = await action.execute(
"<list_directory_files><path>./src</path></list_directory_files>",
);

expect(result.success).toBe(true);
expect(result.data).toBe("file1.txt\nfile2.txt");
});

it("should handle recursive listing", async () => {
mocker.mockPrototype(DirectoryScanner, "scan", {
success: true,
data: "dir1/file1.txt\ndir2/file2.txt",
});

const result = await action.execute(
"<list_directory_files><path>./src</path><recursive>true</recursive></list_directory_files>",
);

expect(result.success).toBe(true);
expect(result.data).toBe("dir1/file1.txt\ndir2/file2.txt");
});

it("should handle multiple paths", async () => {
const scanSpy = mocker.mockPrototypeWith(
DirectoryScanner,
"scan",
async (path: string) => {
if (path === "./src1") {
return { success: true, data: "src1/file1.txt\nsrc1/file2.txt" };
}
return { success: true, data: "src2/file3.txt\nsrc2/file4.txt" };
},
);

const result = await action.execute(
"<list_directory_files><path>./src1</path><path>./src2</path></list_directory_files>",
);

expect(result.success).toBe(true);
expect(result.data).toContain("src1/file1.txt");
expect(result.data).toContain("src1/file2.txt");
expect(result.data).toContain("src2/file3.txt");
expect(result.data).toContain("src2/file4.txt");
expect(scanSpy).toHaveBeenCalledTimes(2);
expect(scanSpy).toHaveBeenCalledWith("./src1", expect.any(Object));
expect(scanSpy).toHaveBeenCalledWith("./src2", expect.any(Object));
});

it("should handle multiple paths with recursive option", async () => {
const scanSpy = mocker.mockPrototypeWith(
DirectoryScanner,
"scan",
async (path: string) => {
if (path === "./src1") {
return { success: true, data: "src1/deep/file1.txt\nsrc1/file2.txt" };
}
return { success: true, data: "src2/deep/file3.txt\nsrc2/file4.txt" };
},
);

const result = await action.execute(
"<list_directory_files><path>./src1</path><path>./src2</path><recursive>true</recursive></list_directory_files>",
);

expect(result.success).toBe(true);
expect(result.data).toContain("src1/deep/file1.txt");
expect(result.data).toContain("src1/file2.txt");
expect(result.data).toContain("src2/deep/file3.txt");
expect(result.data).toContain("src2/file4.txt");
expect(scanSpy).toHaveBeenCalledTimes(2);
expect(scanSpy).toHaveBeenCalledWith(
"./src1",
expect.objectContaining({ maxDepth: undefined }),
);
expect(scanSpy).toHaveBeenCalledWith(
"./src2",
expect.objectContaining({ maxDepth: undefined }),
);
});
});
4 changes: 3 additions & 1 deletion src/services/LLM/actions/blueprints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { endTaskActionBlueprint } from "./endTaskActionBlueprint";
import { fetchUrlActionBlueprint } from "./fetchUrlActionBlueprint";
import { gitDiffActionBlueprint } from "./gitDiffActionBlueprint";
import { gitPRDiffActionBlueprint } from "./gitPRDiffActionBlueprint";
import { listDirectoryFilesActionBlueprint } from "./listDirectoryFilesActionBlueprint";
import { moveFileActionBlueprint } from "./moveFileActionBlueprint";
import { readFileActionBlueprint } from "./readFileActionBlueprint";
import { readDirectoryActionBlueprint } from "./readDirectoryActionBlueprint";
import { readFileActionBlueprint } from "./readFileActionBlueprint";
import { relativePathLookupActionBlueprint } from "./relativePathLookupActionBlueprint";
import {
searchFileActionBlueprint,
Expand All @@ -37,6 +38,7 @@ export const actionsBlueprints = {
[searchFileActionBlueprint.tag]: searchFileActionBlueprint,
[searchStringActionBlueprint.tag]: searchStringActionBlueprint,
[writeFileActionBlueprint.tag]: writeFileActionBlueprint,
[listDirectoryFilesActionBlueprint.tag]: listDirectoryFilesActionBlueprint,
[readDirectoryActionBlueprint.tag]: readDirectoryActionBlueprint,
} as const;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IActionBlueprint } from "../core/IAction";
import { ListDirectoryFilesAction } from "../ListDirectoryFilesAction";
import { ActionPriority } from "../types/ActionPriority";

export const listDirectoryFilesActionBlueprint: IActionBlueprint = {
tag: "list_directory_files",
class: ListDirectoryFilesAction,
description: "Lists all file paths inside a directory",
priority: ActionPriority.HIGH,
canRunInParallel: true,
requiresProcessing: false,
usageExplanation:
"<list_directory_files><path>./src</path></list_directory_files>",
parameters: [
{
name: "path",
required: true,
description: "The directory path to list files from",
validator: (value: unknown): value is string =>
typeof value === "string" && value.length > 0,
},
{
name: "recursive",
required: false,
description: "Whether to list files recursively (default: false)",
validator: (value: unknown): value is boolean =>
typeof value === "boolean",
},
],
};
45 changes: 44 additions & 1 deletion src/services/LLM/phases/blueprints/discoveryPhaseBlueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,49 @@ Only one action per reply. Use tags properly:
<path>file.ts</path>
</read_file>

<execute_command>...</execute_command>
<!-- Run typechecks and tests if needed. -->

<!-- Move to the next phase after completion. Do not do it in the same prompt! -->

Ok, I have enough context to move to the next phase.

<end_phase>
strategy_phase
</end_phase>

</phase_prompt>

## Allowed Actions
<!-- Follow correct tag structure and use only one action per reply. No comments or additional text. -->

REMEMBER: ONLY ONE ACTION PER REPLY!!!

<read_file>
<!-- Read individual files only, not directories -->
<path>path/here</path>
<!-- Do not read the same file multiple times unless changed -->
<!-- Ensure correct <read_file> tag format -->
<!-- Read up to 4 related files -->
<!-- Multiple <path> tags allowed -->
<!-- Use relative paths -->
</read_file>

<list_directory_files>
<!-- One or more paths -->
<path>path/here</path>
<path>path/here/2</path>
<recursive>false</recursive>
<!-- Use this action to LIST all files in a directory. Set recursive to true if you want to list files recursively. -->
</list_directory_files>


<execute_command>
<!-- Use this if you want to explore the codebase further. Examples below: -->
<!-- List files and directories: ls -->
<!-- Detailed directory list: ls -lh -->
<!-- Show current directory path: pwd -->
</execute_command>

<search_string>
<directory>...</directory>
<term>...</term>
Expand All @@ -51,6 +93,7 @@ Only one action per reply. Use tags properly:
</search_file>

<read_directory>
<!-- This will read all files in a directory. -->
<!-- One or more paths -->
<path>directory/path</path>
<path>directory/path/2</path>
Expand Down
Loading
Loading