Merge pull request #4 from RooVetGit/add_rules_files

Read from .clinerules and .cursorrules if present
This commit is contained in:
Matt Rubens
2024-11-03 16:15:51 -05:00
committed by GitHub
7 changed files with 152 additions and 11 deletions

Binary file not shown.

BIN
bin/roo-cline-1.0.2.vsix Normal file

Binary file not shown.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "roo-cline",
"version": "1.0.0",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
"version": "1.0.0",
"version": "1.0.2",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",

View File

@@ -2,7 +2,7 @@
"name": "roo-cline",
"displayName": "Roo Cline",
"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.",
"version": "1.0.1",
"version": "1.0.2",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",

View File

@@ -757,11 +757,7 @@ export class Cline {
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
if (this.customInstructions && this.customInstructions.trim()) {
// 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>
systemPrompt += addCustomInstructions(this.customInstructions)
}
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false) + await addCustomInstructions(this.customInstructions ?? '', cwd)
// 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
if (previousApiReqIndex >= 0) {

View File

@@ -0,0 +1,112 @@
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { addCustomInstructions } from '../system'
// Mock external dependencies
jest.mock('os-name', () => () => 'macOS')
jest.mock('default-shell', () => '/bin/zsh')
jest.mock('os', () => ({
homedir: () => '/Users/test',
...jest.requireActual('os')
}))
describe('system.ts', () => {
let tempDir: string
beforeEach(async () => {
// Create a temporary directory for test files
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cline-test-'))
})
afterEach(async () => {
// Clean up temporary directory after each test
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('addCustomInstructions', () => {
it('should include content from .clinerules and .cursorrules if present', async () => {
// Create test rule files
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests\nUse TypeScript')
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)
// Verify all instructions are included
expect(result).toContain('Base instructions')
expect(result).toContain('Always write tests')
expect(result).toContain('Use TypeScript')
expect(result).toContain('Format code before committing')
expect(result).toContain('Rules from .clinerules:')
expect(result).toContain('Rules from .cursorrules:')
})
it('should handle missing rule files gracefully', async () => {
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)
// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})
it('should handle empty rule files', async () => {
// Create empty rule files
await fs.writeFile(path.join(tempDir, '.clinerules'), '')
await fs.writeFile(path.join(tempDir, '.cursorrules'), '')
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)
// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})
it('should handle whitespace-only rule files', async () => {
// Create rule files with only whitespace
await fs.writeFile(path.join(tempDir, '.clinerules'), ' \n \t ')
await fs.writeFile(path.join(tempDir, '.cursorrules'), ' \n ')
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)
// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})
it('should handle one rule file present and one missing', async () => {
// Create only .clinerules
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)
// Should contain base instructions and .clinerules content
expect(result).toContain('Base instructions')
expect(result).toContain('Always write tests')
expect(result).toContain('Rules from .clinerules:')
expect(result).not.toContain('Rules from .cursorrules:')
})
it('should handle empty custom instructions with rule files', async () => {
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')
const result = await addCustomInstructions('', tempDir)
// Should contain rule file content even with empty custom instructions
expect(result).toContain('Always write tests')
expect(result).toContain('Format code before committing')
expect(result).toContain('Rules from .clinerules:')
expect(result).toContain('Rules from .cursorrules:')
})
it('should return empty string when no instructions or rules exist', async () => {
const result = await addCustomInstructions('', tempDir)
expect(result).toBe('')
})
})
})

View File

@@ -1,6 +1,8 @@
import osName from "os-name"
import defaultShell from "default-shell"
import os from "os"
import fs from 'fs/promises'
import path from 'path'
export const SYSTEM_PROMPT = async (
cwd: string,
@@ -281,13 +283,44 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
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.
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.`
export function addCustomInstructions(customInstructions: string): string {
return `
async function loadRuleFiles(cwd: string): Promise<string> {
const ruleFiles = ['.clinerules', '.cursorrules']
let combinedRules = ''
for (const file of ruleFiles) {
try {
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
if (content.trim()) {
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
}
} catch (err) {
// Silently skip if file doesn't exist
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err
}
}
}
return combinedRules
}
export async function addCustomInstructions(customInstructions: string, cwd: string): Promise<string> {
const ruleFileContent = await loadRuleFiles(cwd)
const allInstructions = [customInstructions.trim()]
if (ruleFileContent && ruleFileContent.trim()) {
allInstructions.push(ruleFileContent.trim())
}
const joinedInstructions = allInstructions.join('\n\n')
return joinedInstructions ? `
====
USER'S CUSTOM INSTRUCTIONS
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.
${customInstructions.trim()}`
${joinedInstructions}`
: ""
}