Skip to content

Commit 9c2c269

Browse files
authored
Merge pull request #4 from RooVetGit/add_rules_files
Read from .clinerules and .cursorrules if present
2 parents 1b4b2fb + 0b3294d commit 9c2c269

File tree

7 files changed

+152
-11
lines changed

7 files changed

+152
-11
lines changed

bin/roo-cline-1.0.1.vsix

-23.8 MB
Binary file not shown.

bin/roo-cline-1.0.2.vsix

5.55 MB
Binary file not shown.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "roo-cline",
33
"displayName": "Roo Cline",
44
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
5-
"version": "1.0.1",
5+
"version": "1.0.2",
66
"icon": "assets/icons/icon.png",
77
"galleryBanner": {
88
"color": "#617A91",

src/core/Cline.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -757,11 +757,7 @@ export class Cline {
757757
}
758758

759759
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
760-
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
761-
if (this.customInstructions && this.customInstructions.trim()) {
762-
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
763-
systemPrompt += addCustomInstructions(this.customInstructions)
764-
}
760+
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false) + await addCustomInstructions(this.customInstructions ?? '', cwd)
765761

766762
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
767763
if (previousApiReqIndex >= 0) {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import fs from 'fs/promises'
2+
import path from 'path'
3+
import os from 'os'
4+
import { addCustomInstructions } from '../system'
5+
6+
// Mock external dependencies
7+
jest.mock('os-name', () => () => 'macOS')
8+
jest.mock('default-shell', () => '/bin/zsh')
9+
jest.mock('os', () => ({
10+
homedir: () => '/Users/test',
11+
...jest.requireActual('os')
12+
}))
13+
14+
describe('system.ts', () => {
15+
let tempDir: string
16+
17+
beforeEach(async () => {
18+
// Create a temporary directory for test files
19+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cline-test-'))
20+
})
21+
22+
afterEach(async () => {
23+
// Clean up temporary directory after each test
24+
await fs.rm(tempDir, { recursive: true, force: true })
25+
})
26+
27+
describe('addCustomInstructions', () => {
28+
it('should include content from .clinerules and .cursorrules if present', async () => {
29+
// Create test rule files
30+
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests\nUse TypeScript')
31+
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')
32+
33+
const customInstructions = 'Base instructions'
34+
const result = await addCustomInstructions(customInstructions, tempDir)
35+
36+
// Verify all instructions are included
37+
expect(result).toContain('Base instructions')
38+
expect(result).toContain('Always write tests')
39+
expect(result).toContain('Use TypeScript')
40+
expect(result).toContain('Format code before committing')
41+
expect(result).toContain('Rules from .clinerules:')
42+
expect(result).toContain('Rules from .cursorrules:')
43+
})
44+
45+
it('should handle missing rule files gracefully', async () => {
46+
const customInstructions = 'Base instructions'
47+
const result = await addCustomInstructions(customInstructions, tempDir)
48+
49+
// Should only contain base instructions
50+
expect(result).toContain('Base instructions')
51+
expect(result).not.toContain('Rules from')
52+
})
53+
54+
it('should handle empty rule files', async () => {
55+
// Create empty rule files
56+
await fs.writeFile(path.join(tempDir, '.clinerules'), '')
57+
await fs.writeFile(path.join(tempDir, '.cursorrules'), '')
58+
59+
const customInstructions = 'Base instructions'
60+
const result = await addCustomInstructions(customInstructions, tempDir)
61+
62+
// Should only contain base instructions
63+
expect(result).toContain('Base instructions')
64+
expect(result).not.toContain('Rules from')
65+
})
66+
67+
it('should handle whitespace-only rule files', async () => {
68+
// Create rule files with only whitespace
69+
await fs.writeFile(path.join(tempDir, '.clinerules'), ' \n \t ')
70+
await fs.writeFile(path.join(tempDir, '.cursorrules'), ' \n ')
71+
72+
const customInstructions = 'Base instructions'
73+
const result = await addCustomInstructions(customInstructions, tempDir)
74+
75+
// Should only contain base instructions
76+
expect(result).toContain('Base instructions')
77+
expect(result).not.toContain('Rules from')
78+
})
79+
80+
it('should handle one rule file present and one missing', async () => {
81+
// Create only .clinerules
82+
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')
83+
84+
const customInstructions = 'Base instructions'
85+
const result = await addCustomInstructions(customInstructions, tempDir)
86+
87+
// Should contain base instructions and .clinerules content
88+
expect(result).toContain('Base instructions')
89+
expect(result).toContain('Always write tests')
90+
expect(result).toContain('Rules from .clinerules:')
91+
expect(result).not.toContain('Rules from .cursorrules:')
92+
})
93+
94+
it('should handle empty custom instructions with rule files', async () => {
95+
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')
96+
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')
97+
98+
const result = await addCustomInstructions('', tempDir)
99+
100+
// Should contain rule file content even with empty custom instructions
101+
expect(result).toContain('Always write tests')
102+
expect(result).toContain('Format code before committing')
103+
expect(result).toContain('Rules from .clinerules:')
104+
expect(result).toContain('Rules from .cursorrules:')
105+
})
106+
107+
it('should return empty string when no instructions or rules exist', async () => {
108+
const result = await addCustomInstructions('', tempDir)
109+
expect(result).toBe('')
110+
})
111+
})
112+
})

src/core/prompts/system.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import osName from "os-name"
22
import defaultShell from "default-shell"
33
import os from "os"
4+
import fs from 'fs/promises'
5+
import path from 'path'
46

57
export const SYSTEM_PROMPT = async (
68
cwd: string,
@@ -281,13 +283,44 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
281283
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
282284
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
283285

284-
export function addCustomInstructions(customInstructions: string): string {
285-
return `
286+
async function loadRuleFiles(cwd: string): Promise<string> {
287+
const ruleFiles = ['.clinerules', '.cursorrules']
288+
let combinedRules = ''
289+
290+
for (const file of ruleFiles) {
291+
try {
292+
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
293+
if (content.trim()) {
294+
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
295+
}
296+
} catch (err) {
297+
// Silently skip if file doesn't exist
298+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
299+
throw err
300+
}
301+
}
302+
}
303+
304+
return combinedRules
305+
}
306+
307+
export async function addCustomInstructions(customInstructions: string, cwd: string): Promise<string> {
308+
const ruleFileContent = await loadRuleFiles(cwd)
309+
const allInstructions = [customInstructions.trim()]
310+
311+
if (ruleFileContent && ruleFileContent.trim()) {
312+
allInstructions.push(ruleFileContent.trim())
313+
}
314+
315+
const joinedInstructions = allInstructions.join('\n\n')
316+
317+
return joinedInstructions ? `
286318
====
287319
288320
USER'S CUSTOM INSTRUCTIONS
289321
290322
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
291323
292-
${customInstructions.trim()}`
324+
${joinedInstructions}`
325+
: ""
293326
}

0 commit comments

Comments
 (0)