Skip to content

Commit 8909ee7

Browse files
ryo-maellipsis-dev[bot]saoudrizwan
authored
Support loading by directory in .clinerules/ (RooCodeInc#2156)
* Support loading by directory in .clinerules/ * Update src/utils/fs.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update docs/prompting/README.md * Add changeset * Add tests of isDirectory * Use relative path in instructions * Remove log --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Saoud Rizwan <[email protected]>
1 parent 3a39436 commit 8909ee7

File tree

5 files changed

+88
-8
lines changed

5 files changed

+88
-8
lines changed

.changeset/honest-deers-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Support for Loading Files from the `.clinerules/` Directory

docs/prompting/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ Cline's system prompt, on the other hand, is not user-editable ([here's where yo
128128
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
129129
- Test and Iterate: Experiment to find what works best for your workflow.
130130

131+
132+
### Support for Loading Files from the `.clinerules/` Directory
133+
All files under the `.clinerules/` directory are recursively loaded, and their contents are merged into clineRulesFileInstructions.
134+
135+
#### Example 1:
136+
```
137+
.clinerules/
138+
├── .local-clinerules
139+
└── .project-clinerules
140+
```
141+
142+
#### Example 2:
143+
```
144+
.clinerules/
145+
├── .clinerules-nextjs
146+
├── .clinerules-serverside
147+
└── tests/
148+
├── .pytest-clinerules
149+
└── .jest-clinerules
150+
```
151+
131152
## Prompting Cline 💬
132153

133154
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.

src/core/Cline.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { getApiMetrics } from "../shared/getApiMetrics"
4646
import { HistoryItem } from "../shared/HistoryItem"
4747
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
4848
import { calculateApiCostAnthropic } from "../utils/cost"
49-
import { fileExistsAtPath } from "../utils/fs"
49+
import { fileExistsAtPath, isDirectory } from "../utils/fs"
5050
import { arePathsEqual, getReadablePath } from "../utils/path"
5151
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
5252
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
@@ -1296,13 +1296,33 @@ export class Cline {
12961296
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
12971297
let clineRulesFileInstructions: string | undefined
12981298
if (await fileExistsAtPath(clineRulesFilePath)) {
1299-
try {
1300-
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
1301-
if (ruleFileContent) {
1302-
clineRulesFileInstructions = `# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
1299+
if (await isDirectory(clineRulesFilePath)) {
1300+
try {
1301+
// Read all files in the .clinerules/ directory.
1302+
const ruleFiles = await fs
1303+
.readdir(clineRulesFilePath, { withFileTypes: true, recursive: true })
1304+
.then((files) => files.filter((file) => file.isFile()))
1305+
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
1306+
const ruleFileContent = await Promise.all(
1307+
ruleFiles.map(async (file) => {
1308+
const ruleFilePath = path.resolve(clineRulesFilePath, file)
1309+
const ruleFilePathRelative = path.relative(cwd, ruleFilePath)
1310+
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
1311+
}),
1312+
).then((contents) => contents.join("\n\n"))
1313+
clineRulesFileInstructions = `# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
1314+
} catch {
1315+
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
1316+
}
1317+
} else {
1318+
try {
1319+
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
1320+
if (ruleFileContent) {
1321+
clineRulesFileInstructions = `# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
1322+
}
1323+
} catch {
1324+
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
13031325
}
1304-
} catch {
1305-
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
13061326
}
13071327
}
13081328

src/utils/fs.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
33
import * as os from "os"
44
import * as path from "path"
55
import "should"
6-
import { createDirectoriesForFile, fileExistsAtPath } from "./fs"
6+
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
77

88
describe("Filesystem Utilities", () => {
99
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
@@ -68,4 +68,24 @@ describe("Filesystem Utilities", () => {
6868
exists.should.be.true()
6969
})
7070
})
71+
describe("isDirectory", () => {
72+
it("should return true for directories", async () => {
73+
await fs.mkdir(tmpDir, { recursive: true })
74+
const isDir = await isDirectory(tmpDir)
75+
isDir.should.be.true()
76+
})
77+
78+
it("should return false for files", async () => {
79+
const testFile = path.join(tmpDir, "test.txt")
80+
await fs.writeFile(testFile, "test")
81+
const isDir = await isDirectory(testFile)
82+
isDir.should.be.false()
83+
})
84+
85+
it("should return false for non-existent paths", async () => {
86+
const nonExistentPath = path.join(tmpDir, "does-not-exist")
87+
const isDir = await isDirectory(nonExistentPath)
88+
isDir.should.be.false()
89+
})
90+
})
7191
})

src/utils/fs.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,17 @@ export async function fileExistsAtPath(filePath: string): Promise<boolean> {
4545
return false
4646
}
4747
}
48+
49+
/**
50+
* Checks if the path is a directory
51+
* @param filePath - The path to check.
52+
* @returns A promise that resolves to true if the path is a directory, false otherwise.
53+
*/
54+
export async function isDirectory(filePath: string): Promise<boolean> {
55+
try {
56+
const stats = await fs.stat(filePath)
57+
return stats.isDirectory()
58+
} catch {
59+
return false
60+
}
61+
}

0 commit comments

Comments
 (0)